feat: initial commit

This commit is contained in:
Janpeter Visser 2026-04-18 14:18:26 +02:00
commit 7d443a004a
76 changed files with 15704 additions and 0 deletions

3
.env Normal file
View file

@ -0,0 +1,3 @@
NEXT_PUBLIC_SUPABASE_URL=https://yntzfgnkrwjlnbaxxkkc.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_AVx1DjyUnRtpqGoyHhVR_A_GCtJVXZh
NEXT_PUBLIC_SUPABASE_SERVICE_KEY=sb_service_AVx1DjyUnRtpqGoyHhVR_A_GCtJVXZh

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_your_key_here

2
.env.local Normal file
View file

@ -0,0 +1,2 @@
NEXT_PUBLIC_SUPABASE_URL=https://yntzfgnkrwjlnbaxxkkc.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_AVx1DjyUnRtpqGoyHhVR_A_GCtJVXZh

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
.next
node_modules
.vercel
dist
coverage
*.log
.DS_Store
next-env.d.ts

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
20.9.0

15
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
"sqltools.connections": [
{
"ssh": "Disabled",
"previewLimit": 50,
"server": "aws-0-eu-west-1.pooler.supabase.com",
"port": 5432,
"driver": "PostgreSQL",
"name": "supabase im 2",
"database": "postgres",
"username": "postgres.yntzfgnkrwjlnbaxxkkc",
"password": "$fzysXR5Z+&g-4#"
}
]
}

50
README.md Normal file
View file

@ -0,0 +1,50 @@
# Inspannings Monitor
Wellness-first webapp voor individuele gebruikers die hun energie willen plannen, uitvoeren en evalueren.
## Stack
- Next.js 16 App Router
- React 19
- TypeScript
- Tailwind CSS
- Vercel als hostingdoel
- Supabase voor database en authenticatie
## Scripts
- `npm run dev`
- `npm run build`
- `npm run start`
- `npm run lint`
## Supabase Auth configuratie
1. Kopieer `.env.example` naar `.env.local`
2. Vul in:
- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`
3. Zet in Supabase Dashboard aan:
- Email/password auth
- Self-signup
- Email confirmation verplicht
4. Voeg redirect URLs toe voor:
- `http://localhost:3000/auth/confirm`
- je Vercel productie-URL
- eventuele preview-URL's die je wilt testen
## Supabase database migraties
Voor `ST-102` staat de eerste databasefundering in:
- `supabase/migrations/20260418_create_profiles_and_user_settings.sql`
Voer deze SQL uit in de Supabase SQL Editor of via de Supabase CLI voordat je
de profile/settings-laag lokaal test.
## Eerstvolgende bouwstappen
1. `ST-201` Ochtendcheck-in UI bouwen
2. `ST-203` Budgetlogica implementeren
3. `ST-301` Activiteitenmodel en planning opzetten
4. `ST-105` RLS-policy tests en hardening afronden

101
app/auth-actions.ts Normal file
View file

@ -0,0 +1,101 @@
"use server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import {
buildPathWithQuery,
getRequestOrigin,
sanitizeNextPath,
} from "@/lib/auth/navigation";
import { createClient } from "@/lib/supabase/server";
import { hasSupabaseEnv } from "@/lib/supabase/config";
function getString(formData: FormData, key: string) {
const value = formData.get(key);
return typeof value === "string" ? value.trim() : "";
}
export async function signInAction(formData: FormData) {
const next = sanitizeNextPath(getString(formData, "next"));
if (!hasSupabaseEnv()) {
redirect(buildPathWithQuery("/login", { error: "auth-not-configured", next }));
}
const email = getString(formData, "email");
const password = getString(formData, "password");
if (!email || !password) {
redirect(buildPathWithQuery("/login", { error: "missing-fields", next }));
}
const supabase = await createClient();
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
const normalizedMessage = error.message.toLowerCase();
const errorParam =
error.code === "email_not_confirmed" ||
normalizedMessage.includes("email not confirmed")
? "email-not-confirmed"
: "invalid-credentials";
redirect(buildPathWithQuery("/login", { error: errorParam, next }));
}
redirect(next);
}
export async function signUpAction(formData: FormData) {
const next = sanitizeNextPath(getString(formData, "next"));
if (!hasSupabaseEnv()) {
redirect(
buildPathWithQuery("/sign-up", { error: "auth-not-configured", next }),
);
}
const email = getString(formData, "email");
const password = getString(formData, "password");
if (!email || !password) {
redirect(buildPathWithQuery("/sign-up", { error: "missing-fields", next }));
}
const supabase = await createClient();
const headerStore = await headers();
const origin = getRequestOrigin(headerStore);
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/confirm?next=${encodeURIComponent(next)}`,
},
});
if (error) {
const normalizedMessage = error.message.toLowerCase();
const errorParam =
error.code === "over_email_send_rate_limit" ||
normalizedMessage.includes("rate limit")
? "signup-rate-limited"
: "signup-failed";
redirect(buildPathWithQuery("/sign-up", { error: errorParam, next }));
}
redirect(buildPathWithQuery("/sign-up", { status: "check-email", next }));
}
export async function signOutAction() {
if (hasSupabaseEnv()) {
const supabase = await createClient();
await supabase.auth.signOut();
}
redirect(buildPathWithQuery("/", { status: "signed-out" }));
}

34
app/auth/confirm/route.ts Normal file
View file

@ -0,0 +1,34 @@
import { type EmailOtpType } from "@supabase/supabase-js";
import { type NextRequest, NextResponse } from "next/server";
import { buildPathWithQuery, sanitizeNextPath } from "@/lib/auth/navigation";
import { hasSupabaseEnv } from "@/lib/supabase/config";
import { createClient } from "@/lib/supabase/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const tokenHash = searchParams.get("token_hash");
const type = searchParams.get("type") as EmailOtpType | null;
const next = sanitizeNextPath(searchParams.get("next"));
if (!hasSupabaseEnv()) {
return NextResponse.redirect(
new URL(buildPathWithQuery("/login", { error: "auth-not-configured" }), request.url),
);
}
if (tokenHash && type) {
const supabase = await createClient();
const { error } = await supabase.auth.verifyOtp({
type,
token_hash: tokenHash,
});
if (!error) {
return NextResponse.redirect(new URL(next, request.url));
}
}
return NextResponse.redirect(
new URL(buildPathWithQuery("/login", { error: "verification-failed" }), request.url),
);
}

204
app/dashboard/page.tsx Normal file
View file

@ -0,0 +1,204 @@
import { redirect } from "next/navigation";
import { signOutAction } from "@/app/auth-actions";
import { sanitizeNextPath } from "@/lib/auth/navigation";
import { getAuthState } from "@/lib/auth/session";
import { getProfileBundleForCurrentUser } from "@/lib/profile/service";
import Link from "next/link";
export const dynamic = "force-dynamic";
type DashboardPageProps = {
searchParams: Promise<Record<string, string | string[] | undefined>>;
};
function getParamValue(
params: Record<string, string | string[] | undefined>,
key: string,
) {
const value = params[key];
return typeof value === "string" ? value : null;
}
function formatToggleState(value: boolean, enabledLabel = "Aan", disabledLabel = "Uit") {
return value ? enabledLabel : disabledLabel;
}
function formatReminderTime(value: string | null) {
return value ? value.slice(0, 5) : "Nog niet ingesteld";
}
function getDashboardNotice(status: string | null) {
if (status === "onboarding-completed") {
return "Je onboarding is opgeslagen. Je basisinstellingen staan nu klaar.";
}
if (status === "onboarding-skipped") {
return "Je hebt de onboarding nu overgeslagen. Je kunt hem later alsnog afronden.";
}
return null;
}
export default async function DashboardPage({ searchParams }: DashboardPageProps) {
const authState = await getAuthState();
const resolvedSearchParams = await searchParams;
if (!authState.isConfigured) {
redirect("/login?error=auth-not-configured");
}
if (!authState.isAuthenticated) {
redirect(`/login?next=${encodeURIComponent(sanitizeNextPath("/dashboard"))}`);
}
const profileBundle = await getProfileBundleForCurrentUser();
if (!profileBundle) {
redirect(`/login?next=${encodeURIComponent(sanitizeNextPath("/dashboard"))}`);
}
const { profile, settings } = profileBundle;
const notice = getDashboardNotice(getParamValue(resolvedSearchParams, "status"));
if (!profile.onboardingSeen) {
redirect("/onboarding");
}
const profileTitle = profile.displayName ?? profile.email ?? authState.email ?? "Ingelogde gebruiker";
const onboardingState = profile.onboardingCompleted ? "Afgerond" : "Nog niet afgerond";
const morningReminderState = settings.morningReminderEnabled
? `Aan om ${formatReminderTime(settings.morningReminderTime)}`
: "Uit";
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(167,201,87,0.22),_transparent_32%),linear-gradient(180deg,_#f5f4ee_0%,_#eef2e6_100%)] px-6 py-10 text-slate-900 sm:px-8">
<div className="mx-auto flex max-w-6xl flex-col gap-8">
{notice ? (
<div className="rounded-[1.5rem] border border-emerald-200 bg-emerald-50 px-5 py-4 text-sm leading-7 text-emerald-900">
{notice}
</div>
) : null}
<header className="flex flex-col gap-5 rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:flex-row sm:items-start sm:justify-between sm:p-8">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Protected route
</p>
<h1 className="mt-3 font-[family-name:var(--font-display)] text-4xl leading-tight">
Dashboard placeholder voor release 1
</h1>
<p className="mt-4 max-w-2xl text-base leading-8 text-slate-700">
Je sessie is server-side gevalideerd en het minimale profielbundle is
nu beschikbaar. Daarmee staat de fundering voor onboarding, settings
en de eerste energieflows klaar.
</p>
</div>
<form action={signOutAction}>
<div className="flex flex-wrap items-center gap-3">
<Link
href="/settings"
className="inline-flex rounded-full border border-black/10 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950"
>
Instellingen
</Link>
<button
type="submit"
className="inline-flex rounded-full border border-emerald-900/15 bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Uitloggen
</button>
</div>
</form>
</header>
<section className="grid gap-5 md:grid-cols-3">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Auth
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
Cookie-based sessie actief
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Gebruiker-ID `{authState.userId}` is server-side gevalideerd via Supabase SSR-auth.
</p>
</article>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Profiel
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
{profileTitle}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Taal `{profile.locale}` en timezone `{profile.timezone}` staan nu per
gebruiker opgeslagen.
</p>
</article>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Onboarding
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
{onboardingState}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Nieuwe accounts starten bewust zonder afgeronde onboarding, zodat
`ST-103` straks een duidelijke eerste flow kan aansturen.
</p>
</article>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Instellingen
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
Punten {formatToggleState(settings.showEnergyPoints, "zichtbaar", "verborgen")}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Ochtendreminder: {morningReminderState}. Reflectieprompts:{" "}
{formatToggleState(settings.reflectionReminderEnabled)}.
</p>
</article>
</section>
{!profile.onboardingCompleted ? (
<section className="flex flex-col gap-4 rounded-[1.75rem] border border-amber-900/15 bg-amber-50 px-6 py-5 text-sm leading-7 text-amber-950 shadow-[0_12px_40px_rgba(146,64,14,0.08)] sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-semibold">Je onboarding is nog niet afgerond.</p>
<p className="mt-1 max-w-2xl text-amber-900">
Je kunt de korte flow later alsnog afronden om je basisinstellingen
en eerste voorkeuren vast te leggen.
</p>
</div>
<Link
href="/onboarding"
className="inline-flex shrink-0 rounded-full bg-amber-950 px-5 py-3 text-sm font-semibold text-amber-50 transition hover:-translate-y-0.5 hover:bg-amber-900"
>
Rond onboarding af
</Link>
</section>
) : (
<section className="flex flex-col gap-4 rounded-[1.75rem] border border-emerald-950/10 bg-emerald-950 px-6 py-5 text-sm leading-7 text-emerald-50 shadow-[0_12px_40px_rgba(6,78,59,0.18)] sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-semibold">Je instellingen kun je nu ook los beheren.</p>
<p className="mt-1 max-w-2xl text-emerald-100/85">
`ST-104` staat nu klaar als aparte route, zodat je reminders,
timezone en zichtbaarheid van punten later zelfstandig kunt aanpassen.
</p>
</div>
<Link
href="/settings"
className="inline-flex shrink-0 rounded-full bg-white px-5 py-3 text-sm font-semibold text-emerald-950 transition hover:-translate-y-0.5 hover:bg-emerald-50"
>
Open instellingen
</Link>
</section>
)}
</div>
</main>
);
}

153
app/globals.css Normal file
View file

@ -0,0 +1,153 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
:root {
--font-display: "Iowan Old Style", "Palatino Linotype", "URW Palladio L",
Palatino, Georgia, serif;
--font-body: "Inter", "Aptos", "Segoe UI", "Helvetica Neue", Arial,
sans-serif;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
* {
box-sizing: border-box;
}
html {
background: var(--background);
color: var(--foreground);
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--font-body), sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
@theme inline {
--font-heading: var(--font-sans);
--font-sans: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}

24
app/layout.tsx Normal file
View file

@ -0,0 +1,24 @@
import type { Metadata } from "next";
import "./globals.css";
import { Geist } from "next/font/google";
import { cn } from "@/lib/utils";
const geist = Geist({subsets:['latin'],variable:'--font-sans'});
export const metadata: Metadata = {
title: "Inspannings Monitor",
description:
"Wellness-first app voor energieplanning, zelfreflectie en een rustige plan-doe-evalueer flow.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="nl" className={cn("font-sans", geist.variable)}>
<body>{children}</body>
</html>
);
}

96
app/login/page.tsx Normal file
View file

@ -0,0 +1,96 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { AuthNotice } from "@/components/auth/auth-notice";
import { AuthPanel } from "@/components/auth/auth-panel";
import { signInAction } from "@/app/auth-actions";
import { getAuthNotice } from "@/lib/auth/messages";
import { buildPathWithQuery, sanitizeNextPath } from "@/lib/auth/navigation";
import { getAuthState } from "@/lib/auth/session";
export const dynamic = "force-dynamic";
type LoginPageProps = {
searchParams: Promise<Record<string, string | string[] | undefined>>;
};
function getParamValue(
params: Record<string, string | string[] | undefined>,
key: string,
) {
const value = params[key];
return typeof value === "string" ? value : null;
}
export default async function LoginPage({ searchParams }: LoginPageProps) {
const authState = await getAuthState();
const resolvedSearchParams = await searchParams;
const next = sanitizeNextPath(getParamValue(resolvedSearchParams, "next"));
if (authState.isAuthenticated) {
redirect(next);
}
const notice = getAuthNotice(
getParamValue(resolvedSearchParams, "error"),
getParamValue(resolvedSearchParams, "status"),
);
const signUpHref = buildPathWithQuery("/sign-up", { next });
return (
<AuthPanel
eyebrow="Inloggen"
title="Welkom terug"
description="Log in om je dashboard te openen en je wellness-first flow verder op te bouwen."
footer={
<p>
Nog geen account?{" "}
<Link href={signUpHref} className="font-semibold text-emerald-900">
Maak er een aan
</Link>
</p>
}
>
<AuthNotice notice={notice} />
{!authState.isConfigured ? (
<div className="rounded-2xl border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Voeg eerst je Supabase-gegevens toe in `.env.local` op basis van `.env.example`.
</div>
) : (
<form action={signInAction} className="space-y-4">
<input type="hidden" name="next" value={next} />
<label className="block text-sm font-medium text-slate-800">
E-mailadres
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
type="email"
name="email"
autoComplete="email"
required
/>
</label>
<label className="block text-sm font-medium text-slate-800">
Wachtwoord
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
type="password"
name="password"
autoComplete="current-password"
minLength={8}
required
/>
</label>
<button
type="submit"
className="inline-flex w-full items-center justify-center rounded-2xl bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Inloggen
</button>
</form>
)}
</AuthPanel>
);
}

39
app/onboarding/actions.ts Normal file
View file

@ -0,0 +1,39 @@
"use server";
import { redirect } from "next/navigation";
import { buildPathWithQuery } from "@/lib/auth/navigation";
import {
completeOnboardingForCurrentUser,
markOnboardingSeenForCurrentUser,
} from "@/lib/profile/service";
import type { OnboardingSubmission } from "@/lib/profile/types";
function getString(formData: FormData, key: string) {
const value = formData.get(key);
return typeof value === "string" ? value.trim() : "";
}
function getBoolean(formData: FormData, key: string) {
return formData.get(key) === "true";
}
function buildOnboardingSubmission(formData: FormData): OnboardingSubmission {
return {
displayName: getString(formData, "displayName") || null,
timezone: getString(formData, "timezone"),
morningReminderEnabled: getBoolean(formData, "morningReminderEnabled"),
morningReminderTime: getString(formData, "morningReminderTime") || null,
reflectionReminderEnabled: getBoolean(formData, "reflectionReminderEnabled"),
showEnergyPoints: getBoolean(formData, "showEnergyPoints"),
};
}
export async function completeOnboardingAction(formData: FormData) {
await completeOnboardingForCurrentUser(buildOnboardingSubmission(formData));
redirect(buildPathWithQuery("/dashboard", { status: "onboarding-completed" }));
}
export async function skipOnboardingAction() {
await markOnboardingSeenForCurrentUser();
redirect(buildPathWithQuery("/dashboard", { status: "onboarding-skipped" }));
}

37
app/onboarding/page.tsx Normal file
View file

@ -0,0 +1,37 @@
import { redirect } from "next/navigation";
import { OnboardingFlow } from "@/components/onboarding/onboarding-flow";
import { sanitizeNextPath } from "@/lib/auth/navigation";
import { getAuthState } from "@/lib/auth/session";
import { getProfileBundleForCurrentUser } from "@/lib/profile/service";
export const dynamic = "force-dynamic";
export default async function OnboardingPage() {
const authState = await getAuthState();
if (!authState.isConfigured) {
redirect("/login?error=auth-not-configured");
}
if (!authState.isAuthenticated) {
redirect(`/login?next=${encodeURIComponent(sanitizeNextPath("/onboarding"))}`);
}
const profileBundle = await getProfileBundleForCurrentUser();
if (!profileBundle) {
redirect(`/login?next=${encodeURIComponent(sanitizeNextPath("/onboarding"))}`);
}
if (profileBundle.profile.onboardingCompleted) {
redirect("/dashboard");
}
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(167,201,87,0.22),_transparent_32%),linear-gradient(180deg,_#f5f4ee_0%,_#eef2e6_100%)] px-6 py-10 text-slate-900 sm:px-8">
<div className="mx-auto flex max-w-6xl flex-col gap-8">
<OnboardingFlow profileBundle={profileBundle} />
</div>
</main>
);
}

196
app/page.tsx Normal file
View file

@ -0,0 +1,196 @@
import Link from "next/link";
import { signOutAction } from "@/app/auth-actions";
import { getAuthNotice } from "@/lib/auth/messages";
import { getAuthState } from "@/lib/auth/session";
export const dynamic = "force-dynamic";
const headerActionClassName =
"shrink-0 whitespace-nowrap rounded-full border border-black/10 bg-white/70 px-4 py-2 text-sm font-medium text-slate-900 shadow-sm transition hover:-translate-y-0.5";
const loopSteps = [
{
title: "Check-in",
copy: "Start de dag met een korte energiescore en slaapkwaliteit, zonder overbodige frictie.",
},
{
title: "Plannen",
copy: "Verdeel activiteiten over de dag met een licht energiebudget en duidelijke prioriteiten.",
},
{
title: "Evalueren",
copy: "Kijk rustig terug op wat wel, niet of aangepast is gelukt, zonder medische claims of oordeel.",
},
];
const releaseFocus = [
"Alleen individuele gebruikers in release 1",
"Wellness/self-management positionering",
"Geen sharing, AI of medische workflows in de MVP",
"Vercel + Supabase als technische basis",
];
type HomePageProps = {
searchParams: Promise<Record<string, string | string[] | undefined>>;
};
function getParamValue(
params: Record<string, string | string[] | undefined>,
key: string,
) {
const value = params[key];
return typeof value === "string" ? value : null;
}
export default async function Home({ searchParams }: HomePageProps) {
const authState = await getAuthState();
const resolvedSearchParams = await searchParams;
const notice = getAuthNotice(
getParamValue(resolvedSearchParams, "error"),
getParamValue(resolvedSearchParams, "status"),
);
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(167,201,87,0.22),_transparent_32%),linear-gradient(180deg,_#f5f4ee_0%,_#eef2e6_100%)] text-slate-900">
<div className="mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-10 sm:px-8 lg:px-10">
<header className="mb-10 flex items-center justify-between border-b border-black/10 pb-5">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-600">
Inspannings Monitor
</p>
<h1 className="font-[family-name:var(--font-display)] text-3xl leading-tight sm:text-5xl">
Rustige basis voor een wellness-first MVP
</h1>
</div>
<div className="flex flex-wrap items-center justify-end gap-3">
{authState.isConfigured ? (
authState.isAuthenticated ? (
<>
<Link
href="/dashboard"
className={headerActionClassName}
>
Naar dashboard
</Link>
<form action={signOutAction}>
<button
type="submit"
className={headerActionClassName}
>
Uitloggen
</button>
</form>
</>
) : (
<>
<Link
href="/login"
className={headerActionClassName}
>
Inloggen
</Link>
<Link
href="/sign-up"
className={headerActionClassName}
>
Account aanmaken
</Link>
</>
)
) : (
<span className="rounded-full border border-amber-900/15 bg-amber-50 px-4 py-2 text-sm font-medium text-amber-900 shadow-sm">
Supabase nog niet geconfigureerd
</span>
)}
</div>
</header>
{notice ? (
<div className="mb-6 rounded-[1.5rem] border border-emerald-200 bg-emerald-50 px-5 py-4 text-sm leading-7 text-emerald-900">
{notice.text}
</div>
) : null}
<section className="grid gap-6 lg:grid-cols-[1.35fr_0.95fr]">
<article className="rounded-[2rem] border border-black/10 bg-white/70 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<p className="mb-4 max-w-2xl text-lg leading-8 text-slate-700">
De projectbasis staat nu, inclusief de eerste auth-laag via Supabase.
Release 1 blijft bewust smal: publieke landing, aparte login/signup
routes en een eerste protected dashboard als basis voor de volgende stories.
</p>
<div className="grid gap-4 md:grid-cols-3">
{loopSteps.map((step, index) => (
<section
key={step.title}
className="rounded-[1.5rem] border border-black/8 bg-stone-50 p-5"
>
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.22em] text-slate-500">
Stap {index + 1}
</p>
<h2 className="mb-2 font-[family-name:var(--font-display)] text-2xl">
{step.title}
</h2>
<p className="text-sm leading-7 text-slate-700">{step.copy}</p>
</section>
))}
</div>
</article>
<aside className="rounded-[2rem] border border-emerald-950/10 bg-emerald-950 px-6 py-7 text-emerald-50 shadow-[0_18px_60px_rgba(6,78,59,0.18)] sm:px-8">
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
Release 1 focus
</p>
<ul className="space-y-3">
{releaseFocus.map((item) => (
<li
key={item}
className="rounded-2xl border border-white/10 bg-white/8 px-4 py-3 text-sm leading-7"
>
{item}
</li>
))}
</ul>
{authState.isConfigured ? (
<p className="mt-5 text-sm leading-7 text-emerald-100/80">
Auth is ingericht met e-mail, wachtwoord en verplichte e-mailverificatie.
</p>
) : (
<p className="mt-5 text-sm leading-7 text-emerald-100/80">
Voeg `.env.local` toe om login, signup en protected routes lokaal te activeren.
</p>
)}
</aside>
</section>
<section className="mt-8 grid gap-5 rounded-[2rem] border border-black/10 bg-white/60 p-6 shadow-[0_10px_45px_rgba(71,85,105,0.08)] backdrop-blur sm:grid-cols-2 lg:grid-cols-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Volgende story
</p>
<p className="mt-2 font-semibold text-slate-900">
ST-201 Ochtendcheck-in
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Doelgroep
</p>
<p className="mt-2 font-semibold text-slate-900">Volwassen individuele gebruikers</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Positionering
</p>
<p className="mt-2 font-semibold text-slate-900">Wellness / self-management</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Status
</p>
<p className="mt-2 font-semibold text-slate-900">Auth, onboarding en settings actief</p>
</div>
</section>
</div>
</main>
);
}

31
app/settings/actions.ts Normal file
View file

@ -0,0 +1,31 @@
"use server";
import { redirect } from "next/navigation";
import { buildPathWithQuery } from "@/lib/auth/navigation";
import { saveSettingsForCurrentUser } from "@/lib/profile/service";
import type { SettingsSubmission } from "@/lib/profile/types";
function getString(formData: FormData, key: string) {
const value = formData.get(key);
return typeof value === "string" ? value.trim() : "";
}
function getBoolean(formData: FormData, key: string) {
return formData.get(key) === "true";
}
function buildSettingsSubmission(formData: FormData): SettingsSubmission {
return {
locale: getString(formData, "locale"),
timezone: getString(formData, "timezone"),
morningReminderEnabled: getBoolean(formData, "morningReminderEnabled"),
morningReminderTime: getString(formData, "morningReminderTime") || null,
reflectionReminderEnabled: getBoolean(formData, "reflectionReminderEnabled"),
showEnergyPoints: getBoolean(formData, "showEnergyPoints"),
};
}
export async function saveSettingsAction(formData: FormData) {
await saveSettingsForCurrentUser(buildSettingsSubmission(formData));
redirect(buildPathWithQuery("/settings", { status: "saved" }));
}

138
app/settings/page.tsx Normal file
View file

@ -0,0 +1,138 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { signOutAction } from "@/app/auth-actions";
import { SettingsForm } from "@/components/settings/settings-form";
import { sanitizeNextPath } from "@/lib/auth/navigation";
import { getAuthState } from "@/lib/auth/session";
import { getProfileBundleForCurrentUser } from "@/lib/profile/service";
export const dynamic = "force-dynamic";
type SettingsPageProps = {
searchParams: Promise<Record<string, string | string[] | undefined>>;
};
function getParamValue(
params: Record<string, string | string[] | undefined>,
key: string,
) {
const value = params[key];
return typeof value === "string" ? value : null;
}
function getSettingsNotice(status: string | null) {
if (status === "saved") {
return "Je instellingen zijn opgeslagen.";
}
return null;
}
export default async function SettingsPage({ searchParams }: SettingsPageProps) {
const authState = await getAuthState();
const resolvedSearchParams = await searchParams;
if (!authState.isConfigured) {
redirect("/login?error=auth-not-configured");
}
if (!authState.isAuthenticated) {
redirect(`/login?next=${encodeURIComponent(sanitizeNextPath("/settings"))}`);
}
const profileBundle = await getProfileBundleForCurrentUser();
if (!profileBundle) {
redirect(`/login?next=${encodeURIComponent(sanitizeNextPath("/settings"))}`);
}
if (!profileBundle.profile.onboardingSeen) {
redirect("/onboarding");
}
const notice = getSettingsNotice(getParamValue(resolvedSearchParams, "status"));
const profileTitle =
profileBundle.profile.displayName ??
profileBundle.profile.email ??
authState.email ??
"Ingelogde gebruiker";
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(167,201,87,0.22),_transparent_32%),linear-gradient(180deg,_#f5f4ee_0%,_#eef2e6_100%)] px-6 py-10 text-slate-900 sm:px-8">
<div className="mx-auto flex max-w-6xl flex-col gap-8">
<header className="flex flex-col gap-5 rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:flex-row sm:items-start sm:justify-between sm:p-8">
<div>
<div className="flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
<Link href="/dashboard" className="transition hover:text-slate-900">
Dashboard
</Link>
<span>/</span>
<span>Instellingen</span>
</div>
<h1 className="mt-3 font-[family-name:var(--font-display)] text-4xl leading-tight">
Instellingen
</h1>
<p className="mt-4 max-w-2xl text-base leading-8 text-slate-700">
Pas je basisvoorkeuren rustig aan. Alles blijft beperkt tot jouw eigen
account en de wellness-first scope van release 1.
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<Link
href="/dashboard"
className="rounded-full border border-black/10 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950"
>
Terug naar dashboard
</Link>
<form action={signOutAction}>
<button
type="submit"
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Uitloggen
</button>
</form>
</div>
</header>
{notice ? (
<div className="rounded-[1.5rem] border border-emerald-200 bg-emerald-50 px-5 py-4 text-sm leading-7 text-emerald-900">
{notice}
</div>
) : null}
<section className="grid gap-5 lg:grid-cols-[1.1fr_0.9fr]">
<SettingsForm profileBundle={profileBundle} />
<aside className="space-y-5">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Account
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
{profileTitle}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
E-mailadres: {profileBundle.profile.email ?? authState.email ?? "Onbekend"}
</p>
</article>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Huidige status
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
Onboarding {profileBundle.profile.onboardingCompleted ? "afgerond" : "later afronden"}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Je kunt later altijd terug naar onboarding of direct verder bouwen op
deze voorkeuren in de dagflow.
</p>
</article>
</aside>
</section>
</div>
</main>
);
}

96
app/sign-up/page.tsx Normal file
View file

@ -0,0 +1,96 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { AuthNotice } from "@/components/auth/auth-notice";
import { AuthPanel } from "@/components/auth/auth-panel";
import { signUpAction } from "@/app/auth-actions";
import { getAuthNotice } from "@/lib/auth/messages";
import { buildPathWithQuery, sanitizeNextPath } from "@/lib/auth/navigation";
import { getAuthState } from "@/lib/auth/session";
export const dynamic = "force-dynamic";
type SignUpPageProps = {
searchParams: Promise<Record<string, string | string[] | undefined>>;
};
function getParamValue(
params: Record<string, string | string[] | undefined>,
key: string,
) {
const value = params[key];
return typeof value === "string" ? value : null;
}
export default async function SignUpPage({ searchParams }: SignUpPageProps) {
const authState = await getAuthState();
const resolvedSearchParams = await searchParams;
const next = sanitizeNextPath(getParamValue(resolvedSearchParams, "next"));
if (authState.isAuthenticated) {
redirect(next);
}
const notice = getAuthNotice(
getParamValue(resolvedSearchParams, "error"),
getParamValue(resolvedSearchParams, "status"),
);
const loginHref = buildPathWithQuery("/login", { next });
return (
<AuthPanel
eyebrow="Account aanmaken"
title="Maak je eerste account aan"
description="Release 1 gebruikt e-mail en wachtwoord als eenvoudige basis. Na registratie bevestig je eerst je e-mailadres."
footer={
<p>
Heb je al een account?{" "}
<Link href={loginHref} className="font-semibold text-emerald-900">
Log dan in
</Link>
</p>
}
>
<AuthNotice notice={notice} />
{!authState.isConfigured ? (
<div className="rounded-2xl border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Voeg eerst je Supabase-gegevens toe in `.env.local` op basis van `.env.example`.
</div>
) : (
<form action={signUpAction} className="space-y-4">
<input type="hidden" name="next" value={next} />
<label className="block text-sm font-medium text-slate-800">
E-mailadres
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
type="email"
name="email"
autoComplete="email"
required
/>
</label>
<label className="block text-sm font-medium text-slate-800">
Wachtwoord
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
type="password"
name="password"
autoComplete="new-password"
minLength={8}
required
/>
</label>
<button
type="submit"
className="inline-flex w-full items-center justify-center rounded-2xl bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Account aanmaken
</button>
</form>
)}
</AuthPanel>
);
}

25
components.json Normal file
View file

@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

View file

@ -0,0 +1,25 @@
import { type AuthNotice } from "@/lib/auth/messages";
type AuthNoticeProps = {
notice: AuthNotice | null;
};
const toneStyles = {
error: "border-rose-200 bg-rose-50 text-rose-900",
success: "border-emerald-200 bg-emerald-50 text-emerald-900",
info: "border-sky-200 bg-sky-50 text-sky-900",
};
export function AuthNotice({ notice }: AuthNoticeProps) {
if (!notice) {
return null;
}
return (
<div
className={`mb-5 rounded-2xl border px-4 py-3 text-sm leading-7 ${toneStyles[notice.tone]}`}
>
{notice.text}
</div>
);
}

View file

@ -0,0 +1,64 @@
import Link from "next/link";
import type { ReactNode } from "react";
type AuthPanelProps = {
eyebrow: string;
title: string;
description: string;
children: ReactNode;
footer: ReactNode;
};
export function AuthPanel({
eyebrow,
title,
description,
children,
footer,
}: AuthPanelProps) {
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(167,201,87,0.22),_transparent_32%),linear-gradient(180deg,_#f5f4ee_0%,_#eef2e6_100%)] px-6 py-10 text-slate-900 sm:px-8">
<div className="mx-auto grid min-h-[calc(100vh-5rem)] max-w-6xl gap-6 lg:grid-cols-[1.05fr_0.95fr]">
<section className="flex flex-col justify-between rounded-[2rem] border border-black/10 bg-emerald-950 p-7 text-emerald-50 shadow-[0_18px_60px_rgba(6,78,59,0.18)] sm:p-9">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
{eyebrow}
</p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl leading-tight sm:text-5xl">
{title}
</h1>
<p className="mt-5 max-w-xl text-base leading-8 text-emerald-50/85">
{description}
</p>
</div>
<div className="mt-10 rounded-[1.5rem] border border-white/10 bg-white/8 p-5 text-sm leading-7 text-emerald-50/90">
<p className="font-semibold">Release 1 blijft bewust licht.</p>
<ul className="mt-3 space-y-2">
<li>Wellness-first en alleen voor individuele gebruikers</li>
<li>Geen zorgverlenerstoegang, sharing of AI in deze fase</li>
<li>Authenticatie via Supabase met cookie-based sessies</li>
</ul>
</div>
</section>
<section className="flex items-center">
<div className="w-full rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<div className="mb-6 flex items-center justify-between gap-3">
<Link
href="/"
className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500 transition hover:text-slate-900"
>
Terug naar landing
</Link>
</div>
{children}
<div className="mt-6 border-t border-black/10 pt-5 text-sm text-slate-600">
{footer}
</div>
</div>
</section>
</div>
</main>
);
}

View file

@ -0,0 +1,292 @@
"use client";
import type { MouseEvent } from "react";
import { useState } from "react";
import { completeOnboardingAction, skipOnboardingAction } from "@/app/onboarding/actions";
import { ONBOARDING_TIMEZONE_OPTIONS } from "@/lib/onboarding/options";
import type { ProfileBundle } from "@/lib/profile/types";
type OnboardingFlowProps = {
profileBundle: ProfileBundle;
};
const steps = [
{
eyebrow: "Stap 1",
title: "Zo gebruiken we Inspannings Monitor",
description:
"De app helpt je om je dag rustiger te plannen en terug te kijken zonder medische claims of zorgverlenerfuncties.",
},
{
eyebrow: "Stap 2",
title: "Basisprofiel",
description:
"Kies hoe de app je mag aanspreken en welke timezone het best bij je dagindeling past.",
},
{
eyebrow: "Stap 3",
title: "Startvoorkeuren",
description:
"Kies rustig hoe zichtbaar je energiebudget is en of je lichte reminders wilt ontvangen.",
},
] as const;
export function OnboardingFlow({ profileBundle }: OnboardingFlowProps) {
const [currentStep, setCurrentStep] = useState(0);
const [displayName, setDisplayName] = useState(profileBundle.profile.displayName ?? "");
const [timezone, setTimezone] = useState(profileBundle.profile.timezone);
const [showEnergyPoints, setShowEnergyPoints] = useState(
profileBundle.settings.showEnergyPoints,
);
const [morningReminderEnabled, setMorningReminderEnabled] = useState(
profileBundle.settings.morningReminderEnabled,
);
const [morningReminderTime, setMorningReminderTime] = useState(
profileBundle.settings.morningReminderTime ?? "08:30",
);
const [reflectionReminderEnabled, setReflectionReminderEnabled] = useState(
profileBundle.settings.reflectionReminderEnabled,
);
const step = steps[currentStep];
const isFirstStep = currentStep === 0;
const isLastStep = currentStep === steps.length - 1;
function goToPreviousStep() {
setCurrentStep((stepIndex) => Math.max(0, stepIndex - 1));
}
function goToNextStep(event: MouseEvent<HTMLButtonElement>) {
// This button lives inside the onboarding form. By preventing the default
// click action and rendering a keyed replacement, we avoid an accidental
// form submit when the final step button appears after the state update.
event.preventDefault();
setCurrentStep((stepIndex) => Math.min(steps.length - 1, stepIndex + 1));
}
return (
<div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
<section className="rounded-[2rem] border border-black/10 bg-emerald-950 p-7 text-emerald-50 shadow-[0_18px_60px_rgba(6,78,59,0.18)] sm:p-9">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
{step.eyebrow}
</p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl leading-tight sm:text-5xl">
{step.title}
</h1>
<p className="mt-5 max-w-xl text-base leading-8 text-emerald-50/85">
{step.description}
</p>
<div className="mt-10 rounded-[1.5rem] border border-white/10 bg-white/8 p-5 text-sm leading-7 text-emerald-50/90">
<p className="font-semibold">Release 1 blijft bewust wellness-first.</p>
<ul className="mt-3 space-y-2">
<li>Alleen voor individuele gebruikers, zonder delen of zorgverlenerstoegang.</li>
<li>De app geeft geen diagnose, behandeling of medisch advies.</li>
<li>Bij acute of snel verslechterende klachten hoort directe hulp via arts, huisartsenpost of 112 buiten deze app.</li>
</ul>
</div>
<ol className="mt-8 flex gap-3">
{steps.map((item, index) => (
<li
key={item.title}
className={`h-2 flex-1 rounded-full ${
index <= currentStep ? "bg-emerald-200" : "bg-white/15"
}`}
/>
))}
</ol>
</section>
<section className="rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<div className="mb-6 flex items-center justify-between gap-3">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Korte onboarding
</p>
<form action={skipOnboardingAction}>
<button
type="submit"
className="rounded-full border border-black/10 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950"
>
Nu overslaan
</button>
</form>
</div>
<form action={completeOnboardingAction} className="space-y-6">
<input type="hidden" name="displayName" value={displayName} />
<input type="hidden" name="timezone" value={timezone} />
<input
type="hidden"
name="showEnergyPoints"
value={showEnergyPoints ? "true" : "false"}
/>
<input
type="hidden"
name="morningReminderEnabled"
value={morningReminderEnabled ? "true" : "false"}
/>
<input type="hidden" name="morningReminderTime" value={morningReminderTime} />
<input
type="hidden"
name="reflectionReminderEnabled"
value={reflectionReminderEnabled ? "true" : "false"}
/>
{currentStep === 0 ? (
<div className="space-y-4">
<article className="rounded-[1.5rem] border border-black/10 bg-stone-50 p-5">
<h2 className="font-[family-name:var(--font-display)] text-2xl text-slate-900">
Wat je hier wél krijgt
</h2>
<p className="mt-3 text-sm leading-7 text-slate-700">
Een rustige plan-doe-evalueer flow met energiebudgetten, zonder
druk, score-oordeel of medische terminologie.
</p>
</article>
<article className="rounded-[1.5rem] border border-black/10 bg-stone-50 p-5">
<h2 className="font-[family-name:var(--font-display)] text-2xl text-slate-900">
Wat deze app niet doet
</h2>
<p className="mt-3 text-sm leading-7 text-slate-700">
Geen diagnose, geen behandeling, geen medische triage en geen
automatisch delen met derden.
</p>
</article>
</div>
) : null}
{currentStep === 1 ? (
<div className="space-y-5">
<label className="block text-sm font-medium text-slate-800">
Schermnaam
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
type="text"
value={displayName}
onChange={(event) => setDisplayName(event.target.value)}
placeholder="Optioneel, bijvoorbeeld Jan"
maxLength={40}
/>
</label>
<div className="rounded-[1.5rem] border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Voertaal voor release 1 staat vast op <strong>Nederlands</strong>.
</div>
<label className="block text-sm font-medium text-slate-800">
Timezone
<select
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
value={timezone}
onChange={(event) => setTimezone(event.target.value)}
>
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
) : null}
{currentStep === 2 ? (
<div className="space-y-4">
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={showEnergyPoints}
onChange={(event) => setShowEnergyPoints(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Toon energiebudgetpunten
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Laat geplande en resterende punten zichtbaar zien in de interface.
</span>
</span>
</label>
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={morningReminderEnabled}
onChange={(event) => setMorningReminderEnabled(event.target.checked)}
/>
<span className="flex-1">
<span className="block text-sm font-semibold text-slate-900">
Zet een lichte ochtendreminder aan
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Handig als je later een korte check-in wilt doen zonder extra druk.
</span>
{morningReminderEnabled ? (
<input
className="mt-3 w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-base outline-none transition focus:border-emerald-600"
type="time"
value={morningReminderTime}
onChange={(event) => setMorningReminderTime(event.target.value)}
/>
) : null}
</span>
</label>
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={reflectionReminderEnabled}
onChange={(event) => setReflectionReminderEnabled(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Sta lichte reflectieprompts toe
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Optionele terugblikprompts kunnen later helpen om rustiger patronen te zien.
</span>
</span>
</label>
</div>
) : null}
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-black/10 pt-6">
<button
type="button"
onClick={goToPreviousStep}
disabled={isFirstStep}
className="rounded-full border border-black/10 bg-white px-5 py-3 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950 disabled:cursor-not-allowed disabled:opacity-45"
>
Vorige
</button>
{isLastStep ? (
<button
key="complete-onboarding"
type="submit"
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Rond onboarding af
</button>
) : (
<button
key={`next-step-${currentStep}`}
type="button"
onClick={goToNextStep}
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Ga verder
</button>
)}
</div>
</form>
</section>
</div>
);
}

View file

@ -0,0 +1,217 @@
"use client";
import { useState } from "react";
import { saveSettingsAction } from "@/app/settings/actions";
import { ONBOARDING_TIMEZONE_OPTIONS } from "@/lib/onboarding/options";
import type { ProfileBundle } from "@/lib/profile/types";
type SettingsFormProps = {
profileBundle: ProfileBundle;
};
const LOCALE_OPTIONS = [
{
value: "nl-NL",
label: "Nederlands",
},
] as const;
export function SettingsForm({ profileBundle }: SettingsFormProps) {
const [locale, setLocale] = useState(profileBundle.profile.locale);
const [timezone, setTimezone] = useState(profileBundle.profile.timezone);
const [showEnergyPoints, setShowEnergyPoints] = useState(
profileBundle.settings.showEnergyPoints,
);
const [morningReminderEnabled, setMorningReminderEnabled] = useState(
profileBundle.settings.morningReminderEnabled,
);
const [morningReminderTime, setMorningReminderTime] = useState(
profileBundle.settings.morningReminderTime ?? "08:30",
);
const [reflectionReminderEnabled, setReflectionReminderEnabled] = useState(
profileBundle.settings.reflectionReminderEnabled,
);
return (
<form action={saveSettingsAction} className="space-y-6">
<input type="hidden" name="locale" value={locale} />
<input type="hidden" name="timezone" value={timezone} />
<input
type="hidden"
name="showEnergyPoints"
value={showEnergyPoints ? "true" : "false"}
/>
<input
type="hidden"
name="morningReminderEnabled"
value={morningReminderEnabled ? "true" : "false"}
/>
<input type="hidden" name="morningReminderTime" value={morningReminderTime} />
<input
type="hidden"
name="reflectionReminderEnabled"
value={reflectionReminderEnabled ? "true" : "false"}
/>
<section className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Account
</p>
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-slate-900">
Basisinstellingen voor jouw account
</h2>
<p className="mt-3 max-w-2xl text-sm leading-7 text-slate-700">
Je past hier alleen je wellness-first voorkeuren aan. Er zijn in release 1
geen medische velden, deelinstellingen of zorgverlenerrollen.
</p>
</section>
<section className="grid gap-5 lg:grid-cols-2">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Taal en tijd
</p>
<div className="mt-5 space-y-5">
<label className="block text-sm font-medium text-slate-800">
Taal
<select
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
value={locale}
onChange={(event) => setLocale(event.target.value)}
>
{LOCALE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<div className="rounded-[1.5rem] border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Release 1 draait bewust volledig in het Nederlands. De taalinstelling
blijft al wel aanwezig in het accountmodel.
</div>
<label className="block text-sm font-medium text-slate-800">
Timezone
<select
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
value={timezone}
onChange={(event) => setTimezone(event.target.value)}
>
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
</article>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Interface
</p>
<div className="mt-5 space-y-4">
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={showEnergyPoints}
onChange={(event) => setShowEnergyPoints(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Toon energiebudgetpunten
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Laat budgetpunten zichtbaar zien in het dashboard en latere dagflows.
</span>
</span>
</label>
</div>
</article>
</section>
<section className="grid gap-5 lg:grid-cols-2">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Reminders
</p>
<div className="mt-5 space-y-4">
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={morningReminderEnabled}
onChange={(event) => setMorningReminderEnabled(event.target.checked)}
/>
<span className="flex-1">
<span className="block text-sm font-semibold text-slate-900">
Ochtendreminder
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Zet een lichte reminder aan voor een rustige start van je check-in.
</span>
{morningReminderEnabled ? (
<input
className="mt-3 w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-base outline-none transition focus:border-emerald-600"
type="time"
value={morningReminderTime}
onChange={(event) => setMorningReminderTime(event.target.value)}
/>
) : null}
</span>
</label>
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={reflectionReminderEnabled}
onChange={(event) => setReflectionReminderEnabled(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Reflectieprompts toestaan
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Maak alvast de opt-in klaar voor lichte terugblikprompts in een latere story.
</span>
</span>
</label>
</div>
</article>
<article className="rounded-[1.75rem] border border-emerald-950/10 bg-emerald-950 p-6 text-emerald-50 shadow-[0_12px_40px_rgba(6,78,59,0.18)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
Bewuste grenzen
</p>
<ul className="mt-4 space-y-3 text-sm leading-7 text-emerald-50/90">
<li>Geen medische drempels, diagnoses of behandelinstellingen.</li>
<li>Geen delen met zorgverleners of naasten in release 1.</li>
<li>Alle instellingen blijven gekoppeld aan alleen jouw account.</li>
</ul>
</article>
</section>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-black/10 pt-6">
<p className="text-sm leading-7 text-slate-600">
Wijzigingen zijn direct van toepassing op jouw account en volgende sessies.
</p>
<button
type="submit"
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
Instellingen opslaan
</button>
</div>
</form>
);
}

58
components/ui/button.tsx Normal file
View file

@ -0,0 +1,58 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

53
docs/README.md Normal file
View file

@ -0,0 +1,53 @@
# Inspannings Monitor Documentatieset
Deze map bevat de vernieuwde documentatie voor de gekozen `wellness/self-management` route van `Inspannings Monitor`, met een expliciet opengehouden pad naar een latere medische producttrack.
## Huidige leidende documenten
- [inspannings-monitor-01-productkader-en-positionering-v06.docx](./inspannings-monitor-01-productkader-en-positionering-v06.docx)
Legt intended use, non-intended use, scope, doelgroep en claim-guardrails vast.
- [inspannings-monitor-02-functionele-specificatie-mvp-v06.docx](./inspannings-monitor-02-functionele-specificatie-mvp-v06.docx)
Beschrijft de wellness-first MVP in toetsbare functionele requirements.
- [inspannings-monitor-03-privacy-security-safety-baseline-v02.docx](./inspannings-monitor-03-privacy-security-safety-baseline-v02.docx)
Bundelt de minimale randvoorwaarden voor privacy, informatiebeveiliging en productveiligheid.
- [inspannings-monitor-04-roadmap-wellness-naar-medisch-v02.docx](./inspannings-monitor-04-roadmap-wellness-naar-medisch-v02.docx)
Laat zien hoe het product gecontroleerd kan doorgroeien zonder ongemerkt medische scope binnen te trekken.
- [inspannings-monitor-05-technische-architectuur-en-implementatie-v01.docx](./inspannings-monitor-05-technische-architectuur-en-implementatie-v01.docx)
Brengt de technische implementatielaag uit de oude specificatie terug als apart architectuurdocument voor de wellness-first MVP.
- [inspannings-monitor-06-implementatieplan-en-backlog-v01.docx](./inspannings-monitor-06-implementatieplan-en-backlog-v01.docx)
Zet de documentatieset om naar een uitvoerbare backlog met epics, stories, afhankelijkheden en releasevolgorde.
## Bevestigde uitgangspunten
- Productnaam: `Inspannings Monitor`
- Positionering eerste release: `wellness/self-management`
- Release 1: alleen individuele gebruikers
- Doelgroep: `volwassenen`
- Voertaal eerste release: `Nederlands`
- Hosting: `Vercel`
- Database: `Supabase PostgreSQL`
- Authenticatie: `Supabase Auth`
## Generator
- [generate_inspannings_monitor_docs.py](./generate_inspannings_monitor_docs.py)
Genereert de actuele `.docx`-documenten opnieuw vanuit de bevestigde uitgangspunten.
## Backlog en Linear
- [backlog/inspannings-monitor-backlog.md](./backlog/inspannings-monitor-backlog.md)
- [backlog/inspannings-monitor-backlog.csv](./backlog/inspannings-monitor-backlog.csv)
- [backlog/inspannings-monitor-linear-setup.md](./backlog/inspannings-monitor-linear-setup.md)
- [backlog/inspannings-monitor-linear-import-checklist.md](./backlog/inspannings-monitor-linear-import-checklist.md)
- [backlog/inspannings-monitor-linear-projects.md](./backlog/inspannings-monitor-linear-projects.md)
- [backlog/inspannings-monitor-linear-eerste-30-minuten.md](./backlog/inspannings-monitor-linear-eerste-30-minuten.md)
- [backlog/generate_linear_backlog_assets.py](./backlog/generate_linear_backlog_assets.py)
## Oudere documenten
De eerdere `EnergyPace`-documenten in deze map zijn niet leidend meer. De `Inspannings Monitor`-documenten hierboven zijn de actuele basis.

View file

@ -0,0 +1,332 @@
import csv
from pathlib import Path
BASE_DIR = Path("/Users/janpetervisser/Development/third/docs/backlog")
SOURCE_CSV = BASE_DIR / "inspannings-monitor-backlog.csv"
LINEAR_CSV = BASE_DIR / "inspannings-monitor-linear-import-issues.csv"
PROJECTS_CSV = BASE_DIR / "inspannings-monitor-linear-projects.csv"
TEAM_NAME = "Inspannings Monitor"
INITIATIVE_NAME = "Release 1 MVP"
CREATED_AT = "2026-04-17T00:00:00Z"
LINEAR_HEADERS = [
"ID",
"Team",
"Title",
"Description",
"Status",
"Estimate",
"Priority",
"Project ID",
"Project",
"Creator",
"Assignee",
"Labels",
"Cycle Number",
"Cycle Name",
"Cycle Start",
"Cycle End",
"Created",
"Updated",
"Started",
"Triaged",
"Completed",
"Canceled",
"Archived",
"Due Date",
"Parent issue",
"Initiatives",
"Project Milestone ID",
"Project Milestone",
"SLA Status",
]
PRIORITY_MAP = {
"P0": "high",
"P1": "medium",
}
def read_rows():
with SOURCE_CSV.open(newline="", encoding="utf-8") as handle:
return list(csv.DictReader(handle))
def build_epic_map(rows):
return {
row["ID"]: row["Title"]
for row in rows
if row.get("Issue Type") == "Epic"
}
def normalize_labels(raw):
labels = [label.strip() for label in raw.split(";") if label.strip()]
return ", ".join(labels)
def build_description(row, epic_title):
parts = [
row["Description"].strip(),
"",
"## Context",
f"- Bron backlog-ID: `{row['ID']}`",
f"- Epic / project: `{epic_title}`",
f"- Fase: `{row['Phase']}`",
]
depends_on = row.get("Depends On", "").strip()
if depends_on:
parts.append(f"- Afhankelijk van: `{depends_on}`")
definition = row.get("Definition of Done", "").strip()
if definition:
parts.extend(
[
"",
"## Definition of done",
definition,
]
)
return "\n".join(parts).strip()
def build_linear_rows(rows):
epic_map = build_epic_map(rows)
linear_rows = []
for row in rows:
if row.get("Issue Type") != "Story":
continue
epic_id = row.get("Epic", "").strip()
epic_title = epic_map.get(epic_id, "")
labels = normalize_labels(row.get("Labels", ""))
linear_rows.append(
{
"ID": "",
"Team": TEAM_NAME,
"Title": row["Title"].strip(),
"Description": build_description(row, epic_title),
"Status": "Backlog",
"Estimate": "",
"Priority": PRIORITY_MAP.get(row.get("Priority", "").strip(), ""),
"Project ID": "",
"Project": epic_title,
"Creator": "",
"Assignee": "",
"Labels": labels,
"Cycle Number": "",
"Cycle Name": "",
"Cycle Start": "",
"Cycle End": "",
"Created": CREATED_AT,
"Updated": "",
"Started": "",
"Triaged": "",
"Completed": "",
"Canceled": "",
"Archived": "",
"Due Date": "",
"Parent issue": "",
"Initiatives": INITIATIVE_NAME,
"Project Milestone ID": "",
"Project Milestone": "",
"SLA Status": "",
}
)
return linear_rows
def write_linear_csv(rows):
with LINEAR_CSV.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=LINEAR_HEADERS)
writer.writeheader()
writer.writerows(rows)
def write_projects_csv():
headers = [
"Name",
"Summary",
"Status",
"Milestones",
"Creator",
"Lead",
"Members",
"Created At",
"Started At",
"Target Date",
"Completed At",
"Canceled At",
"Teams",
"Initiatives",
]
rows = [
{
"Name": "Fundament",
"Summary": "Leg de technische basis voor release 1 met projectsetup, omgevingen, UI-basis en foutafhandeling.",
"Status": "Planned",
"Milestones": "",
"Creator": "",
"Lead": "",
"Members": "",
"Created At": CREATED_AT,
"Started At": "",
"Target Date": "",
"Completed At": "",
"Canceled At": "",
"Teams": TEAM_NAME,
"Initiatives": INITIATIVE_NAME,
},
{
"Name": "Auth en profiel",
"Summary": "Implementeer accounttoegang, profieldata, onboarding en basisinstellingen per gebruiker.",
"Status": "Planned",
"Milestones": "",
"Creator": "",
"Lead": "",
"Members": "",
"Created At": CREATED_AT,
"Started At": "",
"Target Date": "",
"Completed At": "",
"Canceled At": "",
"Teams": TEAM_NAME,
"Initiatives": INITIATIVE_NAME,
},
{
"Name": "Ochtendcheck-in",
"Summary": "Bouw de ochtendcheck-in met energiescore, slaapkwaliteit en automatische budgetafleiding.",
"Status": "Planned",
"Milestones": "",
"Creator": "",
"Lead": "",
"Members": "",
"Created At": CREATED_AT,
"Started At": "",
"Target Date": "",
"Completed At": "",
"Canceled At": "",
"Teams": TEAM_NAME,
"Initiatives": INITIATIVE_NAME,
},
{
"Name": "Dagplanning",
"Summary": "Maak plannen van activiteiten mogelijk met budgetfeedback, energie-impact en prioriteit.",
"Status": "Planned",
"Milestones": "",
"Creator": "",
"Lead": "",
"Members": "",
"Created At": CREATED_AT,
"Started At": "",
"Target Date": "",
"Completed At": "",
"Canceled At": "",
"Teams": TEAM_NAME,
"Initiatives": INITIATIVE_NAME,
},
{
"Name": "Evaluatie en dagoverzicht",
"Summary": "Maak evaluatie van activiteiten en een dagelijks overzicht van gepland versus uitgevoerd mogelijk.",
"Status": "Planned",
"Milestones": "",
"Creator": "",
"Lead": "",
"Members": "",
"Created At": CREATED_AT,
"Started At": "",
"Target Date": "",
"Completed At": "",
"Canceled At": "",
"Teams": TEAM_NAME,
"Initiatives": INITIATIVE_NAME,
},
{
"Name": "Weekoverzicht en inzichten",
"Summary": "Voeg weekterugblik, eenvoudige aggregaties en veilige patroonweergave toe zonder medische claims.",
"Status": "Backlog",
"Milestones": "",
"Creator": "",
"Lead": "",
"Members": "",
"Created At": CREATED_AT,
"Started At": "",
"Target Date": "",
"Completed At": "",
"Canceled At": "",
"Teams": TEAM_NAME,
"Initiatives": INITIATIVE_NAME,
},
{
"Name": "Reflectie en reminders",
"Summary": "Voeg optionele T+1/T+2 reflectieprompts en lichte reminderlogica toe voor zwaardere dagen.",
"Status": "Backlog",
"Milestones": "",
"Creator": "",
"Lead": "",
"Members": "",
"Created At": CREATED_AT,
"Started At": "",
"Target Date": "",
"Completed At": "",
"Canceled At": "",
"Teams": TEAM_NAME,
"Initiatives": INITIATIVE_NAME,
},
{
"Name": "Security en operations",
"Summary": "Borg logging, rate limiting, secrets, back-up en owner-only toegangscontrole voor echte gebruikersintroductie.",
"Status": "Planned",
"Milestones": "",
"Creator": "",
"Lead": "",
"Members": "",
"Created At": CREATED_AT,
"Started At": "",
"Target Date": "",
"Completed At": "",
"Canceled At": "",
"Teams": TEAM_NAME,
"Initiatives": INITIATIVE_NAME,
},
{
"Name": "Launch-readiness",
"Summary": "Rond QA, copy review, accessibility checks, DPIA-input en go-live checks voor release 1 af.",
"Status": "Backlog",
"Milestones": "",
"Creator": "",
"Lead": "",
"Members": "",
"Created At": CREATED_AT,
"Started At": "",
"Target Date": "",
"Completed At": "",
"Canceled At": "",
"Teams": TEAM_NAME,
"Initiatives": INITIATIVE_NAME,
},
]
with PROJECTS_CSV.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=headers)
writer.writeheader()
writer.writerows(rows)
def main():
rows = read_rows()
linear_rows = build_linear_rows(rows)
write_linear_csv(linear_rows)
write_projects_csv()
print(LINEAR_CSV)
print(f"rows={len(linear_rows)}")
print(PROJECTS_CSV)
print("projects=9")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,53 @@
"ID","Title","Description","Issue Type","Epic","Priority","Status","Phase","Labels","Depends On","Definition of Done"
"EPIC-01","Fundament","Projectbasis, omgevingen en design foundation neerzetten.","Epic","","P0","Todo","Release 1","release:r1;domain:platform","",""
"ST-001","Next.js projectbasis opzetten","Zet de projectbasis op met TypeScript en de gekozen stylingaanpak.","Story","EPIC-01","P0","Todo","Release 1","release:r1;epic:fundament;type:build","EPIC-01","Project start lokaal en in preview zonder handmatige workarounds."
"ST-002","Omgevingen definiëren","Richt development, preview en production technisch in.","Story","EPIC-01","P0","Todo","Release 1","release:r1;epic:fundament;type:ops","ST-001","Development, preview en production zijn technisch ingericht."
"ST-003","Component foundation neerzetten","Bouw herbruikbare basiscomponenten voor formulieren, kaarten, knoppen en meldingen.","Story","EPIC-01","P0","Todo","Release 1","release:r1;epic:fundament;type:ui","ST-001","Herbruikbare basiscomponenten zijn mobiel bruikbaar."
"ST-004","Foutafhandeling en lege staten ontwerpen","Ontwerp en implementeer lege staten en bruikbare foutfeedback.","Story","EPIC-01","P0","Todo","Release 1","release:r1;epic:fundament;type:ux","ST-001","Gebruiker krijgt bruikbare feedback bij lege of foutieve situaties."
"EPIC-02","Auth en profiel","Inloggen, sessies, profiel en basisinstellingen werkend maken.","Epic","","P0","Todo","Release 1","release:r1;domain:auth","EPIC-01",""
"ST-101","Supabase Auth integreren","Integreer Supabase Auth en de sessieflow in de app.","Story","EPIC-02","P0","Todo","Release 1","release:r1;epic:auth-profiel;type:build","EPIC-01","Gebruiker kan inloggen en beveiligde routes gebruiken."
"ST-102","Profile- en UserSettings-model implementeren","Implementeer profiel- en settingsmodellen per gebruiker.","Story","EPIC-02","P0","Todo","Release 1","release:r1;epic:auth-profiel;type:build","ST-101","Profiel en instellingen zijn per gebruiker beschikbaar."
"ST-103","Onboardingflow bouwen","Bouw een onboarding van maximaal drie schermen.","Story","EPIC-02","P0","Todo","Release 1","release:r1;epic:auth-profiel;type:ux","ST-101","Nieuwe gebruiker begrijpt schaal, positionering en basisinstellingen."
"ST-104","Settingsscherm bouwen","Bouw instellingen voor taal, timezone, reminders en zichtbaarheid van punten.","Story","EPIC-02","P0","Todo","Release 1","release:r1;epic:auth-profiel;type:build","ST-102","Taal, timezone, reminders en zichtbaarheid van punten zijn persistent."
"ST-105","RLS-basispolicies inrichten","Richt owner-only RLS-policies in voor profiel en instellingen.","Story","EPIC-02","P0","Todo","Release 1","release:r1;epic:auth-profiel;type:security","ST-101","Gebruiker kan uitsluitend eigen profiel en settings lezen of wijzigen."
"EPIC-03","Ochtendcheck-in","Energiescore, slaapkwaliteit en dagbudget implementeren.","Epic","","P0","Todo","Release 1","release:r1;domain:checkin","EPIC-02",""
"ST-201","EnergySlider en SleepQualityInput bouwen","Bouw de invoercomponenten voor energiescore en slaapkwaliteit.","Story","EPIC-03","P0","Todo","Release 1","release:r1;epic:ochtendcheckin;type:ui","EPIC-02","Check-in kan mobiel comfortabel worden ingevuld."
"ST-202","Server action voor createMorningCheckIn","Implementeer de server action voor het opslaan van de ochtendcheck-in.","Story","EPIC-03","P0","Todo","Release 1","release:r1;epic:ochtendcheckin;type:build","ST-201","Check-in wordt opgeslagen met juiste validatie."
"ST-203","Budgetlogica implementeren","Bouw mapping van score naar energy level en dagbudget.","Story","EPIC-03","P0","Todo","Release 1","release:r1;epic:ochtendcheckin;type:logic","ST-202","Score mapping en budgetberekening zijn consistent en testbaar."
"ST-204","Check-instatus op dashboard tonen","Toon direct score, niveau en budget op het dashboard.","Story","EPIC-03","P0","Todo","Release 1","release:r1;epic:ochtendcheckin;type:ui","ST-202","Gebruiker ziet direct score, niveau en budget."
"ST-205","Unit tests voor score- en budgetmapping","Voeg tests toe voor grenswaarden en budgetberekening.","Story","EPIC-03","P0","Todo","Release 1","release:r1;epic:ochtendcheckin;type:qa","ST-203","Belangrijkste grenswaarden zijn afgedekt."
"EPIC-04","Dagplanning","Activiteiten plannen en budgetfeedback tonen.","Epic","","P0","Todo","Release 1","release:r1;domain:planning","EPIC-03",""
"ST-301","Datamodel voor activiteiten implementeren","Implementeer tabellen en seed-data voor activiteiten, categorieen en skip-redenen.","Story","EPIC-04","P0","Todo","Release 1","release:r1;epic:dagplanning;type:build","EPIC-03","Migraties en seed-data voor categorieen en skip-redenen zijn aanwezig."
"ST-302","Planningformulier bouwen","Bouw het formulier voor naam, categorie, duur, impact en prioriteit.","Story","EPIC-04","P0","Todo","Release 1","release:r1;epic:dagplanning;type:ui","ST-301","Activiteit kan met naam, categorie, duur, impact en prioriteit worden aangemaakt."
"ST-303","Autocomplete op eerdere activiteiten toevoegen","Maak snelle herselectie van eerder gebruikte activiteiten mogelijk.","Story","EPIC-04","P0","Todo","Release 1","release:r1;epic:dagplanning;type:ux","ST-302","Veelgebruikte activiteiten zijn snel opnieuw te kiezen."
"ST-304","EnergyMeter en lopend totaal implementeren","Toon het lopende totaal ten opzichte van het dagbudget.","Story","EPIC-04","P0","Todo","Release 1","release:r1;epic:dagplanning;type:logic-ui","ST-302","Totaal update direct na elke wijziging."
"ST-305","Overschrijdingswaarschuwing toevoegen","Toon een niet-blokkerende waarschuwing bij budgetoverschrijding.","Story","EPIC-04","P0","Todo","Release 1","release:r1;epic:dagplanning;type:ux","ST-304","Gebruiker krijgt feedback maar behoudt regie."
"EPIC-05","Evaluatie en dagoverzicht","Activiteiten afronden en dagresultaat tonen.","Epic","","P0","Todo","Release 1","release:r1;domain:evaluatie","EPIC-04",""
"ST-401","Statusflows voor uitgevoerd, geskipt en aangepast bouwen","Implementeer de drie kernstatussen voor activiteiten.","Story","EPIC-05","P0","Todo","Release 1","release:r1;epic:evaluatie;type:build","EPIC-04","Alle drie de statussen worden correct opgeslagen."
"ST-402","Evaluatievelden toevoegen","Voeg contextuele velden toe voor werkelijke duur, fatigue en skip-reden.","Story","EPIC-05","P0","Todo","Release 1","release:r1;epic:evaluatie;type:ui","ST-401","Contextuele velden verschijnen passend per status."
"ST-403","Ongeplande activiteiten ondersteunen","Maak het mogelijk een ongeplande activiteit toe te voegen en mee te tellen.","Story","EPIC-05","P0","Todo","Release 1","release:r1;epic:evaluatie;type:build","ST-401","Ongeplande activiteit telt mee in werkelijke totalen."
"ST-404","Dagoverzicht bouwen","Bouw het overzicht met gepland versus uitgevoerd en statusverdeling.","Story","EPIC-05","P0","Todo","Release 1","release:r1;epic:evaluatie;type:ui","ST-401","Gepland versus uitgevoerd en statusverdeling zijn zichtbaar."
"ST-405","Dagaggregaties server-side implementeren","Bereken dagtotalen en samenvatting server-side.","Story","EPIC-05","P0","Todo","Release 1","release:r1;epic:evaluatie;type:logic","ST-404","Dagtotalen blijven consistent met individuele records."
"EPIC-06","Weekoverzicht en inzichten","Weekpatronen en veilige insightregels toevoegen.","Epic","","P1","Todo","Release 1","release:r1;domain:insights","EPIC-05",""
"ST-501","Weekoverzichtspagina bouwen","Bouw de pagina voor weekterugblik.","Story","EPIC-06","P1","Todo","Release 1","release:r1;epic:weekoverzicht;type:ui","EPIC-05","Gebruiker kan per week terugkijken."
"ST-502","Weekaggregaties bouwen","Bereken gemiddelde energie en budget-adherence per week.","Story","EPIC-06","P1","Todo","Release 1","release:r1;epic:weekoverzicht;type:logic","ST-501","Gemiddelde energie en budget-adherence zijn herleidbaar en testbaar."
"ST-503","Skip-patronen zichtbaar maken","Toon patronen rond skip-redenen en terugkerende activiteiten.","Story","EPIC-06","P1","Todo","Release 1","release:r1;epic:weekoverzicht;type:logic-ui","ST-502","Patronen worden alleen bij voldoende data getoond."
"ST-504","Insightregels met datadrempels definiëren","Leg guardrails vast voor het tonen van patronen.","Story","EPIC-06","P1","Todo","Release 1","release:r1;epic:weekoverzicht;type:safety-logic","ST-502","Geen patroonclaim zonder guardrails."
"ST-505","Insightcopy toetsen op niet-medische formulering","Controleer alle inzichtteksten op wellness-positionering.","Story","EPIC-06","P1","Todo","Release 1","release:r1;epic:weekoverzicht;type:content","ST-504","Alle teksten blijven binnen wellness-positionering."
"EPIC-07","Reflectie en reminders","Optionele T+1/T+2 follow-up mogelijk maken.","Epic","","P1","Todo","Release 1","release:r1;domain:reminders","EPIC-05",""
"ST-601","ReflectionCheckIn-model en flow implementeren","Implementeer model en basisflow voor reflectie na een zwaardere dag.","Story","EPIC-07","P1","Todo","Release 1","release:r1;epic:reflectie;type:build","EPIC-05","Reflecties kunnen aan eerdere dagen gekoppeld worden."
"ST-602","Joblogica voor T+1/T+2 prompts bouwen","Bepaal server-side welke gebruikers een reflectieprompt moeten zien.","Story","EPIC-07","P1","Todo","Release 1","release:r1;epic:reflectie;type:logic-ops","ST-601","Prompts worden niet dubbel of willekeurig aangemaakt."
"ST-603","Instellingsoptie voor reflectieprompts toevoegen","Maak opt-in beheerbaar vanuit instellingen.","Story","EPIC-07","P1","Todo","Release 1","release:r1;epic:reflectie;type:build","ST-104","Gebruiker beheert opt-in zelfstandig."
"ST-604","Korte reflectie-UI bouwen","Bouw een lichte, niet-medische reflectieprompt.","Story","EPIC-07","P1","Todo","Release 1","release:r1;epic:reflectie;type:ui","ST-602","Prompt voelt licht en niet medisch."
"EPIC-08","Security en operations","Logging, hardening, back-up en policy-tests.","Epic","","P0","Todo","Release 1","release:r1;domain:security-ops","EPIC-01,EPIC-02,EPIC-03,EPIC-04,EPIC-05,EPIC-06,EPIC-07",""
"ST-701","Rate limiting toevoegen","Bescherm kritieke auth- en mutatieroutes tegen misbruik.","Story","EPIC-08","P0","Todo","Release 1","release:r1;epic:security-ops;type:security","EPIC-02","Kritieke auth- en mutatieroutes zijn beschermd."
"ST-702","Logging voor fouten en kernmutaties inrichten","Log fouten, loginproblemen en belangrijke mutaties centraal.","Story","EPIC-08","P0","Todo","Release 1","release:r1;epic:security-ops;type:ops","EPIC-03,EPIC-04,EPIC-05","Kerngebeurtenissen zijn herleidbaar."
"ST-703","Back-up en herstelstrategie documenteren en testen","Werk het restore-pad uit en valideer het.","Story","EPIC-08","P0","Todo","Release 1","release:r1;epic:security-ops;type:ops","EPIC-01","Restore-pad is aantoonbaar gevalideerd."
"ST-704","Secrets- en environmentbeheer formaliseren","Leg veilig beheer van secrets en omgevingen vast voor Vercel en Supabase.","Story","EPIC-08","P0","Todo","Release 1","release:r1;epic:security-ops;type:security-ops","EPIC-01","Geen secrets in code of onveilige configuratie."
"ST-705","RLS-policy tests toevoegen","Test aantoonbaar dat owner-only toegang technisch afgedwongen is.","Story","EPIC-08","P0","Todo","Release 1","release:r1;epic:security-ops;type:qa-security","ST-105","Owner-only model is aantoonbaar afgedwongen."
"EPIC-09","Launch-readiness","QA, copy review, DPIA-input en go-live checks afronden.","Epic","","P0","Todo","Release 1","release:r1;domain:launch","EPIC-01,EPIC-02,EPIC-03,EPIC-04,EPIC-05,EPIC-06,EPIC-07,EPIC-08",""
"ST-801","Kernflows handmatig testen","Voer end-to-end handmatige tests uit op mobiel en desktop.","Story","EPIC-09","P0","Todo","Release 1","release:r1;epic:launch;type:qa","EPIC-05,EPIC-06,EPIC-07","Belangrijkste user journeys zijn geverifieerd."
"ST-802","Accessibility check uitvoeren","Controleer touch targets, contrast en reduced motion.","Story","EPIC-09","P0","Todo","Release 1","release:r1;epic:launch;type:qa-ux","EPIC-05","Touch targets, contrast en reduced motion zijn gecontroleerd."
"ST-803","Copy review doen","Controleer onboarding, dashboardteksten en inzichten op wellness-copy.","Story","EPIC-09","P0","Todo","Release 1","release:r1;epic:launch;type:content-safety","EPIC-06","Geen medische of zorgdossier-taal in release 1."
"ST-804","DPIA-input en datacatalogus afronden","Rond privacyartefacten af op basis van de werkelijke MVP-scope.","Story","EPIC-09","P0","Todo","Release 1","release:r1;epic:launch;type:privacy","EPIC-08","Pre-launch privacyartefacten zijn gereed."
"ST-805","Go-live checklist opstellen","Maak een checklist voor launch, rollback, monitoring en incidentrespons.","Story","EPIC-09","P0","Todo","Release 1","release:r1;epic:launch;type:ops","EPIC-08","Team weet hoe launch en eerste incidentrespons verloopt."
1 ID Title Description Issue Type Epic Priority Status Phase Labels Depends On Definition of Done
2 EPIC-01 Fundament Projectbasis, omgevingen en design foundation neerzetten. Epic P0 Todo Release 1 release:r1;domain:platform
3 ST-001 Next.js projectbasis opzetten Zet de projectbasis op met TypeScript en de gekozen stylingaanpak. Story EPIC-01 P0 Todo Release 1 release:r1;epic:fundament;type:build EPIC-01 Project start lokaal en in preview zonder handmatige workarounds.
4 ST-002 Omgevingen definiëren Richt development, preview en production technisch in. Story EPIC-01 P0 Todo Release 1 release:r1;epic:fundament;type:ops ST-001 Development, preview en production zijn technisch ingericht.
5 ST-003 Component foundation neerzetten Bouw herbruikbare basiscomponenten voor formulieren, kaarten, knoppen en meldingen. Story EPIC-01 P0 Todo Release 1 release:r1;epic:fundament;type:ui ST-001 Herbruikbare basiscomponenten zijn mobiel bruikbaar.
6 ST-004 Foutafhandeling en lege staten ontwerpen Ontwerp en implementeer lege staten en bruikbare foutfeedback. Story EPIC-01 P0 Todo Release 1 release:r1;epic:fundament;type:ux ST-001 Gebruiker krijgt bruikbare feedback bij lege of foutieve situaties.
7 EPIC-02 Auth en profiel Inloggen, sessies, profiel en basisinstellingen werkend maken. Epic P0 Todo Release 1 release:r1;domain:auth EPIC-01
8 ST-101 Supabase Auth integreren Integreer Supabase Auth en de sessieflow in de app. Story EPIC-02 P0 Todo Release 1 release:r1;epic:auth-profiel;type:build EPIC-01 Gebruiker kan inloggen en beveiligde routes gebruiken.
9 ST-102 Profile- en UserSettings-model implementeren Implementeer profiel- en settingsmodellen per gebruiker. Story EPIC-02 P0 Todo Release 1 release:r1;epic:auth-profiel;type:build ST-101 Profiel en instellingen zijn per gebruiker beschikbaar.
10 ST-103 Onboardingflow bouwen Bouw een onboarding van maximaal drie schermen. Story EPIC-02 P0 Todo Release 1 release:r1;epic:auth-profiel;type:ux ST-101 Nieuwe gebruiker begrijpt schaal, positionering en basisinstellingen.
11 ST-104 Settingsscherm bouwen Bouw instellingen voor taal, timezone, reminders en zichtbaarheid van punten. Story EPIC-02 P0 Todo Release 1 release:r1;epic:auth-profiel;type:build ST-102 Taal, timezone, reminders en zichtbaarheid van punten zijn persistent.
12 ST-105 RLS-basispolicies inrichten Richt owner-only RLS-policies in voor profiel en instellingen. Story EPIC-02 P0 Todo Release 1 release:r1;epic:auth-profiel;type:security ST-101 Gebruiker kan uitsluitend eigen profiel en settings lezen of wijzigen.
13 EPIC-03 Ochtendcheck-in Energiescore, slaapkwaliteit en dagbudget implementeren. Epic P0 Todo Release 1 release:r1;domain:checkin EPIC-02
14 ST-201 EnergySlider en SleepQualityInput bouwen Bouw de invoercomponenten voor energiescore en slaapkwaliteit. Story EPIC-03 P0 Todo Release 1 release:r1;epic:ochtendcheckin;type:ui EPIC-02 Check-in kan mobiel comfortabel worden ingevuld.
15 ST-202 Server action voor createMorningCheckIn Implementeer de server action voor het opslaan van de ochtendcheck-in. Story EPIC-03 P0 Todo Release 1 release:r1;epic:ochtendcheckin;type:build ST-201 Check-in wordt opgeslagen met juiste validatie.
16 ST-203 Budgetlogica implementeren Bouw mapping van score naar energy level en dagbudget. Story EPIC-03 P0 Todo Release 1 release:r1;epic:ochtendcheckin;type:logic ST-202 Score mapping en budgetberekening zijn consistent en testbaar.
17 ST-204 Check-instatus op dashboard tonen Toon direct score, niveau en budget op het dashboard. Story EPIC-03 P0 Todo Release 1 release:r1;epic:ochtendcheckin;type:ui ST-202 Gebruiker ziet direct score, niveau en budget.
18 ST-205 Unit tests voor score- en budgetmapping Voeg tests toe voor grenswaarden en budgetberekening. Story EPIC-03 P0 Todo Release 1 release:r1;epic:ochtendcheckin;type:qa ST-203 Belangrijkste grenswaarden zijn afgedekt.
19 EPIC-04 Dagplanning Activiteiten plannen en budgetfeedback tonen. Epic P0 Todo Release 1 release:r1;domain:planning EPIC-03
20 ST-301 Datamodel voor activiteiten implementeren Implementeer tabellen en seed-data voor activiteiten, categorieen en skip-redenen. Story EPIC-04 P0 Todo Release 1 release:r1;epic:dagplanning;type:build EPIC-03 Migraties en seed-data voor categorieen en skip-redenen zijn aanwezig.
21 ST-302 Planningformulier bouwen Bouw het formulier voor naam, categorie, duur, impact en prioriteit. Story EPIC-04 P0 Todo Release 1 release:r1;epic:dagplanning;type:ui ST-301 Activiteit kan met naam, categorie, duur, impact en prioriteit worden aangemaakt.
22 ST-303 Autocomplete op eerdere activiteiten toevoegen Maak snelle herselectie van eerder gebruikte activiteiten mogelijk. Story EPIC-04 P0 Todo Release 1 release:r1;epic:dagplanning;type:ux ST-302 Veelgebruikte activiteiten zijn snel opnieuw te kiezen.
23 ST-304 EnergyMeter en lopend totaal implementeren Toon het lopende totaal ten opzichte van het dagbudget. Story EPIC-04 P0 Todo Release 1 release:r1;epic:dagplanning;type:logic-ui ST-302 Totaal update direct na elke wijziging.
24 ST-305 Overschrijdingswaarschuwing toevoegen Toon een niet-blokkerende waarschuwing bij budgetoverschrijding. Story EPIC-04 P0 Todo Release 1 release:r1;epic:dagplanning;type:ux ST-304 Gebruiker krijgt feedback maar behoudt regie.
25 EPIC-05 Evaluatie en dagoverzicht Activiteiten afronden en dagresultaat tonen. Epic P0 Todo Release 1 release:r1;domain:evaluatie EPIC-04
26 ST-401 Statusflows voor uitgevoerd, geskipt en aangepast bouwen Implementeer de drie kernstatussen voor activiteiten. Story EPIC-05 P0 Todo Release 1 release:r1;epic:evaluatie;type:build EPIC-04 Alle drie de statussen worden correct opgeslagen.
27 ST-402 Evaluatievelden toevoegen Voeg contextuele velden toe voor werkelijke duur, fatigue en skip-reden. Story EPIC-05 P0 Todo Release 1 release:r1;epic:evaluatie;type:ui ST-401 Contextuele velden verschijnen passend per status.
28 ST-403 Ongeplande activiteiten ondersteunen Maak het mogelijk een ongeplande activiteit toe te voegen en mee te tellen. Story EPIC-05 P0 Todo Release 1 release:r1;epic:evaluatie;type:build ST-401 Ongeplande activiteit telt mee in werkelijke totalen.
29 ST-404 Dagoverzicht bouwen Bouw het overzicht met gepland versus uitgevoerd en statusverdeling. Story EPIC-05 P0 Todo Release 1 release:r1;epic:evaluatie;type:ui ST-401 Gepland versus uitgevoerd en statusverdeling zijn zichtbaar.
30 ST-405 Dagaggregaties server-side implementeren Bereken dagtotalen en samenvatting server-side. Story EPIC-05 P0 Todo Release 1 release:r1;epic:evaluatie;type:logic ST-404 Dagtotalen blijven consistent met individuele records.
31 EPIC-06 Weekoverzicht en inzichten Weekpatronen en veilige insightregels toevoegen. Epic P1 Todo Release 1 release:r1;domain:insights EPIC-05
32 ST-501 Weekoverzichtspagina bouwen Bouw de pagina voor weekterugblik. Story EPIC-06 P1 Todo Release 1 release:r1;epic:weekoverzicht;type:ui EPIC-05 Gebruiker kan per week terugkijken.
33 ST-502 Weekaggregaties bouwen Bereken gemiddelde energie en budget-adherence per week. Story EPIC-06 P1 Todo Release 1 release:r1;epic:weekoverzicht;type:logic ST-501 Gemiddelde energie en budget-adherence zijn herleidbaar en testbaar.
34 ST-503 Skip-patronen zichtbaar maken Toon patronen rond skip-redenen en terugkerende activiteiten. Story EPIC-06 P1 Todo Release 1 release:r1;epic:weekoverzicht;type:logic-ui ST-502 Patronen worden alleen bij voldoende data getoond.
35 ST-504 Insightregels met datadrempels definiëren Leg guardrails vast voor het tonen van patronen. Story EPIC-06 P1 Todo Release 1 release:r1;epic:weekoverzicht;type:safety-logic ST-502 Geen patroonclaim zonder guardrails.
36 ST-505 Insightcopy toetsen op niet-medische formulering Controleer alle inzichtteksten op wellness-positionering. Story EPIC-06 P1 Todo Release 1 release:r1;epic:weekoverzicht;type:content ST-504 Alle teksten blijven binnen wellness-positionering.
37 EPIC-07 Reflectie en reminders Optionele T+1/T+2 follow-up mogelijk maken. Epic P1 Todo Release 1 release:r1;domain:reminders EPIC-05
38 ST-601 ReflectionCheckIn-model en flow implementeren Implementeer model en basisflow voor reflectie na een zwaardere dag. Story EPIC-07 P1 Todo Release 1 release:r1;epic:reflectie;type:build EPIC-05 Reflecties kunnen aan eerdere dagen gekoppeld worden.
39 ST-602 Joblogica voor T+1/T+2 prompts bouwen Bepaal server-side welke gebruikers een reflectieprompt moeten zien. Story EPIC-07 P1 Todo Release 1 release:r1;epic:reflectie;type:logic-ops ST-601 Prompts worden niet dubbel of willekeurig aangemaakt.
40 ST-603 Instellingsoptie voor reflectieprompts toevoegen Maak opt-in beheerbaar vanuit instellingen. Story EPIC-07 P1 Todo Release 1 release:r1;epic:reflectie;type:build ST-104 Gebruiker beheert opt-in zelfstandig.
41 ST-604 Korte reflectie-UI bouwen Bouw een lichte, niet-medische reflectieprompt. Story EPIC-07 P1 Todo Release 1 release:r1;epic:reflectie;type:ui ST-602 Prompt voelt licht en niet medisch.
42 EPIC-08 Security en operations Logging, hardening, back-up en policy-tests. Epic P0 Todo Release 1 release:r1;domain:security-ops EPIC-01,EPIC-02,EPIC-03,EPIC-04,EPIC-05,EPIC-06,EPIC-07
43 ST-701 Rate limiting toevoegen Bescherm kritieke auth- en mutatieroutes tegen misbruik. Story EPIC-08 P0 Todo Release 1 release:r1;epic:security-ops;type:security EPIC-02 Kritieke auth- en mutatieroutes zijn beschermd.
44 ST-702 Logging voor fouten en kernmutaties inrichten Log fouten, loginproblemen en belangrijke mutaties centraal. Story EPIC-08 P0 Todo Release 1 release:r1;epic:security-ops;type:ops EPIC-03,EPIC-04,EPIC-05 Kerngebeurtenissen zijn herleidbaar.
45 ST-703 Back-up en herstelstrategie documenteren en testen Werk het restore-pad uit en valideer het. Story EPIC-08 P0 Todo Release 1 release:r1;epic:security-ops;type:ops EPIC-01 Restore-pad is aantoonbaar gevalideerd.
46 ST-704 Secrets- en environmentbeheer formaliseren Leg veilig beheer van secrets en omgevingen vast voor Vercel en Supabase. Story EPIC-08 P0 Todo Release 1 release:r1;epic:security-ops;type:security-ops EPIC-01 Geen secrets in code of onveilige configuratie.
47 ST-705 RLS-policy tests toevoegen Test aantoonbaar dat owner-only toegang technisch afgedwongen is. Story EPIC-08 P0 Todo Release 1 release:r1;epic:security-ops;type:qa-security ST-105 Owner-only model is aantoonbaar afgedwongen.
48 EPIC-09 Launch-readiness QA, copy review, DPIA-input en go-live checks afronden. Epic P0 Todo Release 1 release:r1;domain:launch EPIC-01,EPIC-02,EPIC-03,EPIC-04,EPIC-05,EPIC-06,EPIC-07,EPIC-08
49 ST-801 Kernflows handmatig testen Voer end-to-end handmatige tests uit op mobiel en desktop. Story EPIC-09 P0 Todo Release 1 release:r1;epic:launch;type:qa EPIC-05,EPIC-06,EPIC-07 Belangrijkste user journeys zijn geverifieerd.
50 ST-802 Accessibility check uitvoeren Controleer touch targets, contrast en reduced motion. Story EPIC-09 P0 Todo Release 1 release:r1;epic:launch;type:qa-ux EPIC-05 Touch targets, contrast en reduced motion zijn gecontroleerd.
51 ST-803 Copy review doen Controleer onboarding, dashboardteksten en inzichten op wellness-copy. Story EPIC-09 P0 Todo Release 1 release:r1;epic:launch;type:content-safety EPIC-06 Geen medische of zorgdossier-taal in release 1.
52 ST-804 DPIA-input en datacatalogus afronden Rond privacyartefacten af op basis van de werkelijke MVP-scope. Story EPIC-09 P0 Todo Release 1 release:r1;epic:launch;type:privacy EPIC-08 Pre-launch privacyartefacten zijn gereed.
53 ST-805 Go-live checklist opstellen Maak een checklist voor launch, rollback, monitoring en incidentrespons. Story EPIC-09 P0 Todo Release 1 release:r1;epic:launch;type:ops EPIC-08 Team weet hoe launch en eerste incidentrespons verloopt.

View file

@ -0,0 +1,160 @@
# Inspannings Monitor Backlog
Dit bestand zet de huidige documentatieset om naar een concrete development backlog voor `release 1`.
## Uitgangspunten
- Productnaam: `Inspannings Monitor`
- Positionering: `wellness/self-management`
- Release 1: alleen individuele gebruikers
- Doelgroep: volwassenen
- Voertaal release 1: Nederlands
- Stack: `Vercel + Supabase Auth + Supabase PostgreSQL`
- Buiten scope release 1: sharing, AI, PDF-export, medische claims, habit-tracking buiten slaapkwaliteit
## Aanbevolen bouwvolgorde
1. Fundament en projectopzet
2. Authenticatie, profiel en instellingen
3. Ochtendcheck-in en budgetlogica
4. Activiteiten plannen
5. Activiteiten evalueren en dagoverzicht
6. Weekoverzicht en inzichten
7. Reflectieprompts en geplande taken
8. Privacy, security, logging en launch-readiness
## Epic-overzicht
| Epic | Titel | Prioriteit | Afhankelijk van | Doel |
| --- | --- | --- | --- | --- |
| EPIC-01 | Fundament | P0 | - | Projectbasis, omgevingen en design foundation neerzetten |
| EPIC-02 | Auth en profiel | P0 | EPIC-01 | Inloggen, sessies, profiel en basisinstellingen |
| EPIC-03 | Ochtendcheck-in | P0 | EPIC-02 | Energiescore, slaapkwaliteit en dagbudget |
| EPIC-04 | Dagplanning | P0 | EPIC-03 | Activiteiten plannen en budgetfeedback tonen |
| EPIC-05 | Evaluatie en dagoverzicht | P0 | EPIC-04 | Activiteiten afronden en dagresultaat tonen |
| EPIC-06 | Weekoverzicht en inzichten | P1 | EPIC-05 | Weekpatronen en veilige insightregels |
| EPIC-07 | Reflectie en reminders | P1 | EPIC-05 | Optionele T+1/T+2 follow-up |
| EPIC-08 | Security en operations | P0 | EPIC-01 t/m EPIC-07 | Logging, hardening, back-up en policy-tests |
| EPIC-09 | Launch-readiness | P0 | EPIC-01 t/m EPIC-08 | QA, copy review, DPIA-input en go-live checks |
## EPIC-01 Fundament
Doel: een stabiele technische basis waarop alle kernflows kunnen landen.
| Story ID | Titel | Type | Definition of done |
| --- | --- | --- | --- |
| ST-001 | Next.js projectbasis opzetten | Build | Project start lokaal en in preview zonder handmatige workarounds |
| ST-002 | Omgevingen definiëren | Ops | Development, preview en production zijn technisch ingericht |
| ST-003 | Component foundation neerzetten | UI | Herbruikbare basiscomponenten zijn mobiel bruikbaar |
| ST-004 | Foutafhandeling en lege staten ontwerpen | UX | Gebruiker krijgt bruikbare feedback bij lege of foutieve situaties |
## EPIC-02 Auth en profiel
Doel: iedere gebruiker kan veilig een eigen account en basisinstellingen beheren.
| Story ID | Titel | Type | Definition of done |
| --- | --- | --- | --- |
| ST-101 | Supabase Auth integreren | Build | Gebruiker kan inloggen en beveiligde routes gebruiken |
| ST-102 | Profile- en UserSettings-model implementeren | Build | Profiel en instellingen zijn per gebruiker beschikbaar |
| ST-103 | Onboardingflow bouwen | UX | Nieuwe gebruiker begrijpt schaal, positionering en basisinstellingen |
| ST-104 | Settingsscherm bouwen | Build | Taal, timezone, reminders en zichtbaarheid van punten zijn persistent |
| ST-105 | RLS-basispolicies inrichten | Security | Gebruiker kan uitsluitend eigen profiel en settings lezen of wijzigen |
## EPIC-03 Ochtendcheck-in
Doel: de gebruiker kan met minimale inspanning de dag starten en een budget krijgen.
| Story ID | Titel | Type | Definition of done |
| --- | --- | --- | --- |
| ST-201 | EnergySlider en SleepQualityInput bouwen | UI | Check-in kan mobiel comfortabel worden ingevuld |
| ST-202 | Server action voor createMorningCheckIn | Build | Check-in wordt opgeslagen met juiste validatie |
| ST-203 | Budgetlogica implementeren | Logic | Score mapping en budgetberekening zijn consistent en testbaar |
| ST-204 | Check-instatus op dashboard tonen | UI | Gebruiker ziet direct score, niveau en budget |
| ST-205 | Unit tests voor score- en budgetmapping | QA | Belangrijkste grenswaarden zijn afgedekt |
## EPIC-04 Dagplanning
Doel: de gebruiker kan activiteiten voor de dag plannen binnen een eenvoudig energiemodel.
| Story ID | Titel | Type | Definition of done |
| --- | --- | --- | --- |
| ST-301 | Datamodel voor activiteiten implementeren | Build | Migraties en seed-data voor categorieën en skip-redenen zijn aanwezig |
| ST-302 | Planningformulier bouwen | UI | Activiteit kan met naam, categorie, duur, impact en prioriteit worden aangemaakt |
| ST-303 | Autocomplete op eerdere activiteiten toevoegen | UX | Veelgebruikte activiteiten zijn snel opnieuw te kiezen |
| ST-304 | EnergyMeter en lopend totaal implementeren | Logic/UI | Totaal update direct na elke wijziging |
| ST-305 | Overschrijdingswaarschuwing toevoegen | UX | Gebruiker krijgt feedback maar behoudt regie |
## EPIC-05 Evaluatie en dagoverzicht
Doel: de kernloop afronden door geplande activiteiten te evalueren en terug te zien.
| Story ID | Titel | Type | Definition of done |
| --- | --- | --- | --- |
| ST-401 | Statusflows voor uitgevoerd, geskipt en aangepast bouwen | Build | Alle drie de statussen worden correct opgeslagen |
| ST-402 | Evaluatievelden toevoegen | UI | Contextuele velden verschijnen passend per status |
| ST-403 | Ongeplande activiteiten ondersteunen | Build | Ongeplande activiteit telt mee in werkelijke totalen |
| ST-404 | Dagoverzicht bouwen | UI | Gepland versus uitgevoerd en statusverdeling zijn zichtbaar |
| ST-405 | Dagaggregaties server-side implementeren | Logic | Dagtotalen blijven consistent met individuele records |
## EPIC-06 Weekoverzicht en inzichten
Doel: terugkijken op patronen zonder de wellness-guardrails te verlaten.
| Story ID | Titel | Type | Definition of done |
| --- | --- | --- | --- |
| ST-501 | Weekoverzichtspagina bouwen | UI | Gebruiker kan per week terugkijken |
| ST-502 | Weekaggregaties bouwen | Logic | Gemiddelde energie en budget-adherence zijn herleidbaar en testbaar |
| ST-503 | Skip-patronen zichtbaar maken | Logic/UI | Patronen worden alleen bij voldoende data getoond |
| ST-504 | Insightregels met datadrempels definiëren | Safety/Logic | Geen patroonclaim zonder guardrails |
| ST-505 | Insightcopy toetsen op niet-medische formulering | Content | Alle teksten blijven binnen wellness-positionering |
## EPIC-07 Reflectie en reminders
Doel: gebruikers optioneel laten terugblikken na zwaardere dagen.
| Story ID | Titel | Type | Definition of done |
| --- | --- | --- | --- |
| ST-601 | ReflectionCheckIn-model en flow implementeren | Build | Reflecties kunnen aan eerdere dagen gekoppeld worden |
| ST-602 | Joblogica voor T+1/T+2 prompts bouwen | Logic/Ops | Prompts worden niet dubbel of willekeurig aangemaakt |
| ST-603 | Instellingsoptie voor reflectieprompts toevoegen | Build | Gebruiker beheert opt-in zelfstandig |
| ST-604 | Korte reflectie-UI bouwen | UI | Prompt voelt licht en niet medisch |
## EPIC-08 Security en operations
Doel: de wellness-first MVP technisch hard genoeg maken voor echte gebruikers.
| Story ID | Titel | Type | Definition of done |
| --- | --- | --- | --- |
| ST-701 | Rate limiting toevoegen | Security | Kritieke auth- en mutatieroutes zijn beschermd |
| ST-702 | Logging voor fouten en kernmutaties inrichten | Ops | Kerngebeurtenissen zijn herleidbaar |
| ST-703 | Back-up en herstelstrategie documenteren en testen | Ops | Restore-pad is aantoonbaar gevalideerd |
| ST-704 | Secrets- en environmentbeheer formaliseren | Security/Ops | Geen secrets in code of onveilige configuratie |
| ST-705 | RLS-policy tests toevoegen | QA/Security | Owner-only model is aantoonbaar afgedwongen |
## EPIC-09 Launch-readiness
Doel: release 1 verantwoord kunnen opleveren.
| Story ID | Titel | Type | Definition of done |
| --- | --- | --- | --- |
| ST-801 | Kernflows handmatig testen | QA | Belangrijkste user journeys zijn geverifieerd |
| ST-802 | Accessibility check uitvoeren | QA/UX | Touch targets, contrast en reduced motion zijn gecontroleerd |
| ST-803 | Copy review doen | Content/Safety | Geen medische of zorgdossier-taal in release 1 |
| ST-804 | DPIA-input en datacatalogus afronden | Privacy | Pre-launch privacyartefacten zijn gereed |
| ST-805 | Go-live checklist opstellen | Ops | Team weet hoe launch en eerste incidentrespons verloopt |
## Release-level definition of done
- Alle `P0`-epics zijn functioneel afgerond
- Geen blocking bugs in ochtendcheck-in, planning, evaluatie of dashboardflow
- Owner-only toegang is technisch afgedwongen en getest
- Launchcopy blijft binnen wellness/self-management claims
- Privacy- en securitybasis is gereed voor echte gebruikersintroductie
## Niet in release 1
- Viewerrollen, delen met zorgverleners of naasten, en granular sharing
- Habit tracking buiten slaapkwaliteit
- Database-gestuurde vertalingen of extra talen
- AI-inzichten, chatbotfuncties of vrije tekstinterpretatie
- PDF-export, zorgdossierkoppelingen of medical-track features

View file

@ -0,0 +1,110 @@
# Inspannings Monitor: eerste 30 minuten in Linear
Gebruik dit als snelle start nadat je workspace klaar is.
## 0-5 minuten: basis neerzetten
- Open of maak de workspace `Inspannings Monitor`
- Controleer dat je adminrechten hebt
- Maak één team aan: `Inspannings Monitor`
- Laat de issueworkflow simpel:
- `Backlog`
- `Todo`
- `In Progress`
- `Done`
- `Canceled`
- Zet `Cycles` nog niet aan
## 5-10 minuten: release-structuur maken
- Maak de initiative `Release 1 MVP`
- Maak de 9 projects uit [inspannings-monitor-linear-projects.md](./inspannings-monitor-linear-projects.md)
- Gebruik de samenvattingen uit dat document
- Zet deze projects direct op `Planned`:
- `Fundament`
- `Auth en profiel`
- `Ochtendcheck-in`
- `Dagplanning`
- `Evaluatie en dagoverzicht`
- `Security en operations`
- Zet deze op `Backlog`:
- `Weekoverzicht en inzichten`
- `Reflectie en reminders`
- `Launch-readiness`
## 10-15 minuten: labels klaarzetten
Maak minimaal deze labels aan:
- `release:r1`
- `type:build`
- `type:ui`
- `type:logic`
- `type:qa`
- `type:security`
- `type:ops`
- `type:ux`
- `type:content`
- `type:privacy`
Maak daarna de `epic:*` labels uit [inspannings-monitor-linear-setup.md](./inspannings-monitor-linear-setup.md).
## 15-20 minuten: backlog importeren
- Open [inspannings-monitor-linear-import-checklist.md](./inspannings-monitor-linear-import-checklist.md)
- Gebruik [inspannings-monitor-linear-import-issues.csv](./inspannings-monitor-linear-import-issues.csv)
- Importeer de `43` issues
- Controleer direct:
- team klopt
- status is `Backlog`
- prioriteit is `high` of `medium`
- labels zijn meegekomen
## 20-25 minuten: eerste opschoning
- Controleer of issues aan de juiste projects hangen
- Als projectkoppeling ontbreekt:
- filter op `epic:*` label
- koppel issues in bulk aan het juiste project
- Koppel daarna alles aan initiative `Release 1 MVP` als dat nog niet goed staat
- Sorteer de backlog op `Priority`
## 25-30 minuten: eerste views maken
Maak deze saved views:
- `Release 1 - All`
- `P0`
- `Security / Privacy`
- `Launch-readiness`
Als je daarna nog tijd hebt:
- wijs 2 of 3 issues uit `Fundament` toe
- zet alleen die issues op `Todo`
- laat de rest nog in `Backlog`
## Wat je nog niet moet doen
- nog geen cycles aanzetten
- nog geen extra workflowstappen maken
- nog geen sub-teams maken
- nog geen extra releasestructuur toevoegen
- nog geen medical-track werk mengen met release 1
## Beste eerste werkset
Als je meteen wilt starten met uitvoering, begin dan hier:
1. `ST-001` Next.js projectbasis opzetten
2. `ST-002` Omgevingen definiëren
3. `ST-101` Supabase Auth integreren
4. `ST-102` Profile- en UserSettings-model implementeren
## Relevante bestanden
- [inspannings-monitor-linear-setup.md](./inspannings-monitor-linear-setup.md)
- [inspannings-monitor-linear-import-checklist.md](./inspannings-monitor-linear-import-checklist.md)
- [inspannings-monitor-linear-projects.md](./inspannings-monitor-linear-projects.md)
- [inspannings-monitor-linear-import-issues.csv](./inspannings-monitor-linear-import-issues.csv)
- [inspannings-monitor-backlog.md](./inspannings-monitor-backlog.md)

View file

@ -0,0 +1,185 @@
# Inspannings Monitor Linear Import Checklist
Gebruik deze checklist om de backlog en projectstructuur van `Inspannings Monitor` gecontroleerd in `Linear` te krijgen.
## Doel
Na afronding van deze checklist heb je:
- één `Linear` workspace
- één team voor `release 1`
- één initiative voor de MVP
- negen projects voor de epics
- een geïmporteerde issue-backlog voor alle stories
- een eerste kwaliteitscontrole op mapping, labels en prioriteiten
## Benodigde bestanden
- [inspannings-monitor-linear-setup.md](./inspannings-monitor-linear-setup.md)
- [inspannings-monitor-linear-import-issues.csv](./inspannings-monitor-linear-import-issues.csv)
- [inspannings-monitor-linear-projects.csv](./inspannings-monitor-linear-projects.csv)
- [inspannings-monitor-linear-projects.md](./inspannings-monitor-linear-projects.md)
- [inspannings-monitor-backlog.md](./inspannings-monitor-backlog.md)
## Fase 1: Workspace voorbereiden
- [ ] Maak of kies de `Linear` workspace voor `Inspannings Monitor`
- [ ] Bevestig dat je adminrechten hebt in de workspace
- [ ] Maak één top-level team aan met naam `Inspannings Monitor`
- [ ] Laat de teamworkflow in het begin simpel:
- `Backlog`
- `Todo`
- `In Progress`
- `Done`
- `Canceled`
- [ ] Zet `Cycles` nog niet aan
- [ ] Gebruik nog geen extra sub-teams
## Fase 2: Labels voorbereiden
Maak deze labels aan als teamlabels:
- [ ] `release:r1`
- [ ] `type:build`
- [ ] `type:ui`
- [ ] `type:logic`
- [ ] `type:qa`
- [ ] `type:security`
- [ ] `type:ops`
- [ ] `type:ux`
- [ ] `type:content`
- [ ] `type:privacy`
- [ ] `epic:fundament`
- [ ] `epic:auth-profiel`
- [ ] `epic:ochtendcheckin`
- [ ] `epic:dagplanning`
- [ ] `epic:evaluatie`
- [ ] `epic:weekoverzicht`
- [ ] `epic:reflectie`
- [ ] `epic:security-ops`
- [ ] `epic:launch`
## Fase 3: Initiative en projects aanmaken
### Initiative
- [ ] Maak één initiative aan: `Release 1 MVP`
### Projects
Maak deze negen projects aan:
- [ ] `Fundament`
- [ ] `Auth en profiel`
- [ ] `Ochtendcheck-in`
- [ ] `Dagplanning`
- [ ] `Evaluatie en dagoverzicht`
- [ ] `Weekoverzicht en inzichten`
- [ ] `Reflectie en reminders`
- [ ] `Security en operations`
- [ ] `Launch-readiness`
### Aanbevolen projectleads
- [ ] Wijs alleen projectleads toe als die nu al duidelijk zijn
- [ ] Laat anders de leads leeg bij de eerste importfase
## Fase 4: Import voorbereiden
- [ ] Open de actuele import-CSV: [inspannings-monitor-linear-import-issues.csv](./inspannings-monitor-linear-import-issues.csv)
- [ ] Controleer steekproefsgewijs:
- `Team` = `Inspannings Monitor`
- `Status` = `Backlog`
- `Priority` = `high` of `medium`
- `Project` bevat één van de negen epic-projectnamen
- `Initiatives` = `Release 1 MVP`
- [ ] Maak desnoods eerst een testworkspace of testteam als je Linear eerst wilt proefimporteren
## Fase 5: Import uitvoeren
Volgens de actuele `Linear`-documentatie loopt import van “other” bronnen via hun importer/CLI met een CSV in `Linear`-formaat.
- [ ] Ga in Linear naar `Settings > Administration > Import/Export`
- [ ] Kies de importroute voor een generieke / `Other`-bron of `Linear CSV`-aanpak
- [ ] Upload of importeer het bestand `inspannings-monitor-linear-import-issues.csv`
- [ ] Gebruik het team `Inspannings Monitor` als doel
- [ ] Rond de import af
## Fase 6: Directe controle na import
Controleer meteen deze punten:
- [ ] Zijn alle `43` issues aanwezig?
- [ ] Staan de issues in het team `Inspannings Monitor`?
- [ ] Hebben issues status `Backlog`?
- [ ] Zijn prioriteiten zichtbaar als `high` of `medium`?
- [ ] Zijn labels meegekomen?
- [ ] Zijn issues aan de juiste projects gekoppeld?
- [ ] Is de initiative-koppeling zichtbaar?
## Fase 7: Als project- of initiative-koppeling niet goed is meegekomen
Gebruik dan deze fallback:
- [ ] Filter op label `epic:fundament` en koppel alle resultaten in bulk aan project `Fundament`
- [ ] Filter op label `epic:auth-profiel` en koppel in bulk aan `Auth en profiel`
- [ ] Filter op label `epic:ochtendcheckin` en koppel in bulk aan `Ochtendcheck-in`
- [ ] Filter op label `epic:dagplanning` en koppel in bulk aan `Dagplanning`
- [ ] Filter op label `epic:evaluatie` en koppel in bulk aan `Evaluatie en dagoverzicht`
- [ ] Filter op label `epic:weekoverzicht` en koppel in bulk aan `Weekoverzicht en inzichten`
- [ ] Filter op label `epic:reflectie` en koppel in bulk aan `Reflectie en reminders`
- [ ] Filter op label `epic:security-ops` en koppel in bulk aan `Security en operations`
- [ ] Filter op label `epic:launch` en koppel in bulk aan `Launch-readiness`
- [ ] Selecteer daarna alle release-1 issues en koppel ze in bulk aan initiative `Release 1 MVP`
## Fase 8: Eerste opschoning in Linear
- [ ] Sorteer backlog eerst op `Priority`
- [ ] Controleer of alle `P0`-stories als `high` binnengekomen zijn
- [ ] Controleer of alle `P1`-stories als `medium` binnengekomen zijn
- [ ] Archiveer nog niets in deze eerste fase
- [ ] Voeg nog geen cycles toe
- [ ] Voeg nog geen extra workflowstappen toe
## Fase 9: Eerste operationele inrichting
- [ ] Maak een opgeslagen view voor `Release 1 - All`
- [ ] Maak een view voor `P0`
- [ ] Maak een view voor `Security / Privacy`
- [ ] Maak een view voor `Launch-readiness`
- [ ] Maak eventueel één view `My work` of `This week`
## Fase 10: Go / No-Go na import
### Go als dit klopt
- [ ] Alle `43` issues staan in Linear
- [ ] De negen projects bestaan
- [ ] De initiative bestaat
- [ ] Labels en prioriteiten zijn bruikbaar
- [ ] De backlog is zonder extra handwerk te filteren per epic
### No-Go als dit misgaat
- [ ] Issues missen of zijn dubbel geïmporteerd
- [ ] Projectkoppeling is structureel kapot
- [ ] Statusmapping is onbruikbaar
- [ ] CSV blijkt niet goed te matchen met de importer
Als `No-Go`: import verwijderen, mapping aanpassen, en opnieuw importeren.
## Eerste week in Linear
Voor de eerste week zou ik dit simpel houden:
- gebruik alleen `Backlog`, `Todo`, `In Progress`, `Done`
- plan nog geen formele cycles
- werk eerst `EPIC-01` en `EPIC-02` uit
- gebruik labels en projects voor overzicht, niet extra workflowcomplexiteit
## Bronnen
- [Linear Importer docs](https://linear.app/docs/import-issues)
- [Linear Teams docs](https://linear.app/docs/teams)
- [Linear Projects docs](https://linear.app/docs/projects)
- [Linear Priority docs](https://linear.app/docs/priority)

View file

@ -0,0 +1,431 @@
ID,Team,Title,Description,Status,Estimate,Priority,Project ID,Project,Creator,Assignee,Labels,Cycle Number,Cycle Name,Cycle Start,Cycle End,Created,Updated,Started,Triaged,Completed,Canceled,Archived,Due Date,Parent issue,Initiatives,Project Milestone ID,Project Milestone,SLA Status
,Inspannings Monitor,Next.js projectbasis opzetten,"Zet de projectbasis op met TypeScript en de gekozen stylingaanpak.
## Context
- Bron backlog-ID: `ST-001`
- Epic / project: `Fundament`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-01`
## Definition of done
Project start lokaal en in preview zonder handmatige workarounds.",Backlog,,high,,Fundament,,,"release:r1, epic:fundament, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Omgevingen definiëren,"Richt development, preview en production technisch in.
## Context
- Bron backlog-ID: `ST-002`
- Epic / project: `Fundament`
- Fase: `Release 1`
- Afhankelijk van: `ST-001`
## Definition of done
Development, preview en production zijn technisch ingericht.",Backlog,,high,,Fundament,,,"release:r1, epic:fundament, type:ops",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Component foundation neerzetten,"Bouw herbruikbare basiscomponenten voor formulieren, kaarten, knoppen en meldingen.
## Context
- Bron backlog-ID: `ST-003`
- Epic / project: `Fundament`
- Fase: `Release 1`
- Afhankelijk van: `ST-001`
## Definition of done
Herbruikbare basiscomponenten zijn mobiel bruikbaar.",Backlog,,high,,Fundament,,,"release:r1, epic:fundament, type:ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Foutafhandeling en lege staten ontwerpen,"Ontwerp en implementeer lege staten en bruikbare foutfeedback.
## Context
- Bron backlog-ID: `ST-004`
- Epic / project: `Fundament`
- Fase: `Release 1`
- Afhankelijk van: `ST-001`
## Definition of done
Gebruiker krijgt bruikbare feedback bij lege of foutieve situaties.",Backlog,,high,,Fundament,,,"release:r1, epic:fundament, type:ux",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Supabase Auth integreren,"Integreer Supabase Auth en de sessieflow in de app.
## Context
- Bron backlog-ID: `ST-101`
- Epic / project: `Auth en profiel`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-01`
## Definition of done
Gebruiker kan inloggen en beveiligde routes gebruiken.",Backlog,,high,,Auth en profiel,,,"release:r1, epic:auth-profiel, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Profile- en UserSettings-model implementeren,"Implementeer profiel- en settingsmodellen per gebruiker.
## Context
- Bron backlog-ID: `ST-102`
- Epic / project: `Auth en profiel`
- Fase: `Release 1`
- Afhankelijk van: `ST-101`
## Definition of done
Profiel en instellingen zijn per gebruiker beschikbaar.",Backlog,,high,,Auth en profiel,,,"release:r1, epic:auth-profiel, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Onboardingflow bouwen,"Bouw een onboarding van maximaal drie schermen.
## Context
- Bron backlog-ID: `ST-103`
- Epic / project: `Auth en profiel`
- Fase: `Release 1`
- Afhankelijk van: `ST-101`
## Definition of done
Nieuwe gebruiker begrijpt schaal, positionering en basisinstellingen.",Backlog,,high,,Auth en profiel,,,"release:r1, epic:auth-profiel, type:ux",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Settingsscherm bouwen,"Bouw instellingen voor taal, timezone, reminders en zichtbaarheid van punten.
## Context
- Bron backlog-ID: `ST-104`
- Epic / project: `Auth en profiel`
- Fase: `Release 1`
- Afhankelijk van: `ST-102`
## Definition of done
Taal, timezone, reminders en zichtbaarheid van punten zijn persistent.",Backlog,,high,,Auth en profiel,,,"release:r1, epic:auth-profiel, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,RLS-basispolicies inrichten,"Richt owner-only RLS-policies in voor profiel en instellingen.
## Context
- Bron backlog-ID: `ST-105`
- Epic / project: `Auth en profiel`
- Fase: `Release 1`
- Afhankelijk van: `ST-101`
## Definition of done
Gebruiker kan uitsluitend eigen profiel en settings lezen of wijzigen.",Backlog,,high,,Auth en profiel,,,"release:r1, epic:auth-profiel, type:security",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,EnergySlider en SleepQualityInput bouwen,"Bouw de invoercomponenten voor energiescore en slaapkwaliteit.
## Context
- Bron backlog-ID: `ST-201`
- Epic / project: `Ochtendcheck-in`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-02`
## Definition of done
Check-in kan mobiel comfortabel worden ingevuld.",Backlog,,high,,Ochtendcheck-in,,,"release:r1, epic:ochtendcheckin, type:ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Server action voor createMorningCheckIn,"Implementeer de server action voor het opslaan van de ochtendcheck-in.
## Context
- Bron backlog-ID: `ST-202`
- Epic / project: `Ochtendcheck-in`
- Fase: `Release 1`
- Afhankelijk van: `ST-201`
## Definition of done
Check-in wordt opgeslagen met juiste validatie.",Backlog,,high,,Ochtendcheck-in,,,"release:r1, epic:ochtendcheckin, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Budgetlogica implementeren,"Bouw mapping van score naar energy level en dagbudget.
## Context
- Bron backlog-ID: `ST-203`
- Epic / project: `Ochtendcheck-in`
- Fase: `Release 1`
- Afhankelijk van: `ST-202`
## Definition of done
Score mapping en budgetberekening zijn consistent en testbaar.",Backlog,,high,,Ochtendcheck-in,,,"release:r1, epic:ochtendcheckin, type:logic",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Check-instatus op dashboard tonen,"Toon direct score, niveau en budget op het dashboard.
## Context
- Bron backlog-ID: `ST-204`
- Epic / project: `Ochtendcheck-in`
- Fase: `Release 1`
- Afhankelijk van: `ST-202`
## Definition of done
Gebruiker ziet direct score, niveau en budget.",Backlog,,high,,Ochtendcheck-in,,,"release:r1, epic:ochtendcheckin, type:ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Unit tests voor score- en budgetmapping,"Voeg tests toe voor grenswaarden en budgetberekening.
## Context
- Bron backlog-ID: `ST-205`
- Epic / project: `Ochtendcheck-in`
- Fase: `Release 1`
- Afhankelijk van: `ST-203`
## Definition of done
Belangrijkste grenswaarden zijn afgedekt.",Backlog,,high,,Ochtendcheck-in,,,"release:r1, epic:ochtendcheckin, type:qa",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Datamodel voor activiteiten implementeren,"Implementeer tabellen en seed-data voor activiteiten, categorieen en skip-redenen.
## Context
- Bron backlog-ID: `ST-301`
- Epic / project: `Dagplanning`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-03`
## Definition of done
Migraties en seed-data voor categorieen en skip-redenen zijn aanwezig.",Backlog,,high,,Dagplanning,,,"release:r1, epic:dagplanning, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Planningformulier bouwen,"Bouw het formulier voor naam, categorie, duur, impact en prioriteit.
## Context
- Bron backlog-ID: `ST-302`
- Epic / project: `Dagplanning`
- Fase: `Release 1`
- Afhankelijk van: `ST-301`
## Definition of done
Activiteit kan met naam, categorie, duur, impact en prioriteit worden aangemaakt.",Backlog,,high,,Dagplanning,,,"release:r1, epic:dagplanning, type:ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Autocomplete op eerdere activiteiten toevoegen,"Maak snelle herselectie van eerder gebruikte activiteiten mogelijk.
## Context
- Bron backlog-ID: `ST-303`
- Epic / project: `Dagplanning`
- Fase: `Release 1`
- Afhankelijk van: `ST-302`
## Definition of done
Veelgebruikte activiteiten zijn snel opnieuw te kiezen.",Backlog,,high,,Dagplanning,,,"release:r1, epic:dagplanning, type:ux",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,EnergyMeter en lopend totaal implementeren,"Toon het lopende totaal ten opzichte van het dagbudget.
## Context
- Bron backlog-ID: `ST-304`
- Epic / project: `Dagplanning`
- Fase: `Release 1`
- Afhankelijk van: `ST-302`
## Definition of done
Totaal update direct na elke wijziging.",Backlog,,high,,Dagplanning,,,"release:r1, epic:dagplanning, type:logic-ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Overschrijdingswaarschuwing toevoegen,"Toon een niet-blokkerende waarschuwing bij budgetoverschrijding.
## Context
- Bron backlog-ID: `ST-305`
- Epic / project: `Dagplanning`
- Fase: `Release 1`
- Afhankelijk van: `ST-304`
## Definition of done
Gebruiker krijgt feedback maar behoudt regie.",Backlog,,high,,Dagplanning,,,"release:r1, epic:dagplanning, type:ux",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,"Statusflows voor uitgevoerd, geskipt en aangepast bouwen","Implementeer de drie kernstatussen voor activiteiten.
## Context
- Bron backlog-ID: `ST-401`
- Epic / project: `Evaluatie en dagoverzicht`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-04`
## Definition of done
Alle drie de statussen worden correct opgeslagen.",Backlog,,high,,Evaluatie en dagoverzicht,,,"release:r1, epic:evaluatie, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Evaluatievelden toevoegen,"Voeg contextuele velden toe voor werkelijke duur, fatigue en skip-reden.
## Context
- Bron backlog-ID: `ST-402`
- Epic / project: `Evaluatie en dagoverzicht`
- Fase: `Release 1`
- Afhankelijk van: `ST-401`
## Definition of done
Contextuele velden verschijnen passend per status.",Backlog,,high,,Evaluatie en dagoverzicht,,,"release:r1, epic:evaluatie, type:ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Ongeplande activiteiten ondersteunen,"Maak het mogelijk een ongeplande activiteit toe te voegen en mee te tellen.
## Context
- Bron backlog-ID: `ST-403`
- Epic / project: `Evaluatie en dagoverzicht`
- Fase: `Release 1`
- Afhankelijk van: `ST-401`
## Definition of done
Ongeplande activiteit telt mee in werkelijke totalen.",Backlog,,high,,Evaluatie en dagoverzicht,,,"release:r1, epic:evaluatie, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Dagoverzicht bouwen,"Bouw het overzicht met gepland versus uitgevoerd en statusverdeling.
## Context
- Bron backlog-ID: `ST-404`
- Epic / project: `Evaluatie en dagoverzicht`
- Fase: `Release 1`
- Afhankelijk van: `ST-401`
## Definition of done
Gepland versus uitgevoerd en statusverdeling zijn zichtbaar.",Backlog,,high,,Evaluatie en dagoverzicht,,,"release:r1, epic:evaluatie, type:ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Dagaggregaties server-side implementeren,"Bereken dagtotalen en samenvatting server-side.
## Context
- Bron backlog-ID: `ST-405`
- Epic / project: `Evaluatie en dagoverzicht`
- Fase: `Release 1`
- Afhankelijk van: `ST-404`
## Definition of done
Dagtotalen blijven consistent met individuele records.",Backlog,,high,,Evaluatie en dagoverzicht,,,"release:r1, epic:evaluatie, type:logic",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Weekoverzichtspagina bouwen,"Bouw de pagina voor weekterugblik.
## Context
- Bron backlog-ID: `ST-501`
- Epic / project: `Weekoverzicht en inzichten`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-05`
## Definition of done
Gebruiker kan per week terugkijken.",Backlog,,medium,,Weekoverzicht en inzichten,,,"release:r1, epic:weekoverzicht, type:ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Weekaggregaties bouwen,"Bereken gemiddelde energie en budget-adherence per week.
## Context
- Bron backlog-ID: `ST-502`
- Epic / project: `Weekoverzicht en inzichten`
- Fase: `Release 1`
- Afhankelijk van: `ST-501`
## Definition of done
Gemiddelde energie en budget-adherence zijn herleidbaar en testbaar.",Backlog,,medium,,Weekoverzicht en inzichten,,,"release:r1, epic:weekoverzicht, type:logic",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Skip-patronen zichtbaar maken,"Toon patronen rond skip-redenen en terugkerende activiteiten.
## Context
- Bron backlog-ID: `ST-503`
- Epic / project: `Weekoverzicht en inzichten`
- Fase: `Release 1`
- Afhankelijk van: `ST-502`
## Definition of done
Patronen worden alleen bij voldoende data getoond.",Backlog,,medium,,Weekoverzicht en inzichten,,,"release:r1, epic:weekoverzicht, type:logic-ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Insightregels met datadrempels definiëren,"Leg guardrails vast voor het tonen van patronen.
## Context
- Bron backlog-ID: `ST-504`
- Epic / project: `Weekoverzicht en inzichten`
- Fase: `Release 1`
- Afhankelijk van: `ST-502`
## Definition of done
Geen patroonclaim zonder guardrails.",Backlog,,medium,,Weekoverzicht en inzichten,,,"release:r1, epic:weekoverzicht, type:safety-logic",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Insightcopy toetsen op niet-medische formulering,"Controleer alle inzichtteksten op wellness-positionering.
## Context
- Bron backlog-ID: `ST-505`
- Epic / project: `Weekoverzicht en inzichten`
- Fase: `Release 1`
- Afhankelijk van: `ST-504`
## Definition of done
Alle teksten blijven binnen wellness-positionering.",Backlog,,medium,,Weekoverzicht en inzichten,,,"release:r1, epic:weekoverzicht, type:content",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,ReflectionCheckIn-model en flow implementeren,"Implementeer model en basisflow voor reflectie na een zwaardere dag.
## Context
- Bron backlog-ID: `ST-601`
- Epic / project: `Reflectie en reminders`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-05`
## Definition of done
Reflecties kunnen aan eerdere dagen gekoppeld worden.",Backlog,,medium,,Reflectie en reminders,,,"release:r1, epic:reflectie, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Joblogica voor T+1/T+2 prompts bouwen,"Bepaal server-side welke gebruikers een reflectieprompt moeten zien.
## Context
- Bron backlog-ID: `ST-602`
- Epic / project: `Reflectie en reminders`
- Fase: `Release 1`
- Afhankelijk van: `ST-601`
## Definition of done
Prompts worden niet dubbel of willekeurig aangemaakt.",Backlog,,medium,,Reflectie en reminders,,,"release:r1, epic:reflectie, type:logic-ops",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Instellingsoptie voor reflectieprompts toevoegen,"Maak opt-in beheerbaar vanuit instellingen.
## Context
- Bron backlog-ID: `ST-603`
- Epic / project: `Reflectie en reminders`
- Fase: `Release 1`
- Afhankelijk van: `ST-104`
## Definition of done
Gebruiker beheert opt-in zelfstandig.",Backlog,,medium,,Reflectie en reminders,,,"release:r1, epic:reflectie, type:build",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Korte reflectie-UI bouwen,"Bouw een lichte, niet-medische reflectieprompt.
## Context
- Bron backlog-ID: `ST-604`
- Epic / project: `Reflectie en reminders`
- Fase: `Release 1`
- Afhankelijk van: `ST-602`
## Definition of done
Prompt voelt licht en niet medisch.",Backlog,,medium,,Reflectie en reminders,,,"release:r1, epic:reflectie, type:ui",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Rate limiting toevoegen,"Bescherm kritieke auth- en mutatieroutes tegen misbruik.
## Context
- Bron backlog-ID: `ST-701`
- Epic / project: `Security en operations`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-02`
## Definition of done
Kritieke auth- en mutatieroutes zijn beschermd.",Backlog,,high,,Security en operations,,,"release:r1, epic:security-ops, type:security",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Logging voor fouten en kernmutaties inrichten,"Log fouten, loginproblemen en belangrijke mutaties centraal.
## Context
- Bron backlog-ID: `ST-702`
- Epic / project: `Security en operations`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-03,EPIC-04,EPIC-05`
## Definition of done
Kerngebeurtenissen zijn herleidbaar.",Backlog,,high,,Security en operations,,,"release:r1, epic:security-ops, type:ops",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Back-up en herstelstrategie documenteren en testen,"Werk het restore-pad uit en valideer het.
## Context
- Bron backlog-ID: `ST-703`
- Epic / project: `Security en operations`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-01`
## Definition of done
Restore-pad is aantoonbaar gevalideerd.",Backlog,,high,,Security en operations,,,"release:r1, epic:security-ops, type:ops",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Secrets- en environmentbeheer formaliseren,"Leg veilig beheer van secrets en omgevingen vast voor Vercel en Supabase.
## Context
- Bron backlog-ID: `ST-704`
- Epic / project: `Security en operations`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-01`
## Definition of done
Geen secrets in code of onveilige configuratie.",Backlog,,high,,Security en operations,,,"release:r1, epic:security-ops, type:security-ops",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,RLS-policy tests toevoegen,"Test aantoonbaar dat owner-only toegang technisch afgedwongen is.
## Context
- Bron backlog-ID: `ST-705`
- Epic / project: `Security en operations`
- Fase: `Release 1`
- Afhankelijk van: `ST-105`
## Definition of done
Owner-only model is aantoonbaar afgedwongen.",Backlog,,high,,Security en operations,,,"release:r1, epic:security-ops, type:qa-security",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Kernflows handmatig testen,"Voer end-to-end handmatige tests uit op mobiel en desktop.
## Context
- Bron backlog-ID: `ST-801`
- Epic / project: `Launch-readiness`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-05,EPIC-06,EPIC-07`
## Definition of done
Belangrijkste user journeys zijn geverifieerd.",Backlog,,high,,Launch-readiness,,,"release:r1, epic:launch, type:qa",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Accessibility check uitvoeren,"Controleer touch targets, contrast en reduced motion.
## Context
- Bron backlog-ID: `ST-802`
- Epic / project: `Launch-readiness`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-05`
## Definition of done
Touch targets, contrast en reduced motion zijn gecontroleerd.",Backlog,,high,,Launch-readiness,,,"release:r1, epic:launch, type:qa-ux",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Copy review doen,"Controleer onboarding, dashboardteksten en inzichten op wellness-copy.
## Context
- Bron backlog-ID: `ST-803`
- Epic / project: `Launch-readiness`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-06`
## Definition of done
Geen medische of zorgdossier-taal in release 1.",Backlog,,high,,Launch-readiness,,,"release:r1, epic:launch, type:content-safety",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,DPIA-input en datacatalogus afronden,"Rond privacyartefacten af op basis van de werkelijke MVP-scope.
## Context
- Bron backlog-ID: `ST-804`
- Epic / project: `Launch-readiness`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-08`
## Definition of done
Pre-launch privacyartefacten zijn gereed.",Backlog,,high,,Launch-readiness,,,"release:r1, epic:launch, type:privacy",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
,Inspannings Monitor,Go-live checklist opstellen,"Maak een checklist voor launch, rollback, monitoring en incidentrespons.
## Context
- Bron backlog-ID: `ST-805`
- Epic / project: `Launch-readiness`
- Fase: `Release 1`
- Afhankelijk van: `EPIC-08`
## Definition of done
Team weet hoe launch en eerste incidentrespons verloopt.",Backlog,,high,,Launch-readiness,,,"release:r1, epic:launch, type:ops",,,,,2026-04-17T00:00:00Z,,,,,,,,,Release 1 MVP,,,
1 ID Team Title Description Status Estimate Priority Project ID Project Creator Assignee Labels Cycle Number Cycle Name Cycle Start Cycle End Created Updated Started Triaged Completed Canceled Archived Due Date Parent issue Initiatives Project Milestone ID Project Milestone SLA Status
2 Inspannings Monitor Next.js projectbasis opzetten Zet de projectbasis op met TypeScript en de gekozen stylingaanpak. ## Context - Bron backlog-ID: `ST-001` - Epic / project: `Fundament` - Fase: `Release 1` - Afhankelijk van: `EPIC-01` ## Definition of done Project start lokaal en in preview zonder handmatige workarounds. Backlog high Fundament release:r1, epic:fundament, type:build 2026-04-17T00:00:00Z Release 1 MVP
3 Inspannings Monitor Omgevingen definiëren Richt development, preview en production technisch in. ## Context - Bron backlog-ID: `ST-002` - Epic / project: `Fundament` - Fase: `Release 1` - Afhankelijk van: `ST-001` ## Definition of done Development, preview en production zijn technisch ingericht. Backlog high Fundament release:r1, epic:fundament, type:ops 2026-04-17T00:00:00Z Release 1 MVP
4 Inspannings Monitor Component foundation neerzetten Bouw herbruikbare basiscomponenten voor formulieren, kaarten, knoppen en meldingen. ## Context - Bron backlog-ID: `ST-003` - Epic / project: `Fundament` - Fase: `Release 1` - Afhankelijk van: `ST-001` ## Definition of done Herbruikbare basiscomponenten zijn mobiel bruikbaar. Backlog high Fundament release:r1, epic:fundament, type:ui 2026-04-17T00:00:00Z Release 1 MVP
5 Inspannings Monitor Foutafhandeling en lege staten ontwerpen Ontwerp en implementeer lege staten en bruikbare foutfeedback. ## Context - Bron backlog-ID: `ST-004` - Epic / project: `Fundament` - Fase: `Release 1` - Afhankelijk van: `ST-001` ## Definition of done Gebruiker krijgt bruikbare feedback bij lege of foutieve situaties. Backlog high Fundament release:r1, epic:fundament, type:ux 2026-04-17T00:00:00Z Release 1 MVP
6 Inspannings Monitor Supabase Auth integreren Integreer Supabase Auth en de sessieflow in de app. ## Context - Bron backlog-ID: `ST-101` - Epic / project: `Auth en profiel` - Fase: `Release 1` - Afhankelijk van: `EPIC-01` ## Definition of done Gebruiker kan inloggen en beveiligde routes gebruiken. Backlog high Auth en profiel release:r1, epic:auth-profiel, type:build 2026-04-17T00:00:00Z Release 1 MVP
7 Inspannings Monitor Profile- en UserSettings-model implementeren Implementeer profiel- en settingsmodellen per gebruiker. ## Context - Bron backlog-ID: `ST-102` - Epic / project: `Auth en profiel` - Fase: `Release 1` - Afhankelijk van: `ST-101` ## Definition of done Profiel en instellingen zijn per gebruiker beschikbaar. Backlog high Auth en profiel release:r1, epic:auth-profiel, type:build 2026-04-17T00:00:00Z Release 1 MVP
8 Inspannings Monitor Onboardingflow bouwen Bouw een onboarding van maximaal drie schermen. ## Context - Bron backlog-ID: `ST-103` - Epic / project: `Auth en profiel` - Fase: `Release 1` - Afhankelijk van: `ST-101` ## Definition of done Nieuwe gebruiker begrijpt schaal, positionering en basisinstellingen. Backlog high Auth en profiel release:r1, epic:auth-profiel, type:ux 2026-04-17T00:00:00Z Release 1 MVP
9 Inspannings Monitor Settingsscherm bouwen Bouw instellingen voor taal, timezone, reminders en zichtbaarheid van punten. ## Context - Bron backlog-ID: `ST-104` - Epic / project: `Auth en profiel` - Fase: `Release 1` - Afhankelijk van: `ST-102` ## Definition of done Taal, timezone, reminders en zichtbaarheid van punten zijn persistent. Backlog high Auth en profiel release:r1, epic:auth-profiel, type:build 2026-04-17T00:00:00Z Release 1 MVP
10 Inspannings Monitor RLS-basispolicies inrichten Richt owner-only RLS-policies in voor profiel en instellingen. ## Context - Bron backlog-ID: `ST-105` - Epic / project: `Auth en profiel` - Fase: `Release 1` - Afhankelijk van: `ST-101` ## Definition of done Gebruiker kan uitsluitend eigen profiel en settings lezen of wijzigen. Backlog high Auth en profiel release:r1, epic:auth-profiel, type:security 2026-04-17T00:00:00Z Release 1 MVP
11 Inspannings Monitor EnergySlider en SleepQualityInput bouwen Bouw de invoercomponenten voor energiescore en slaapkwaliteit. ## Context - Bron backlog-ID: `ST-201` - Epic / project: `Ochtendcheck-in` - Fase: `Release 1` - Afhankelijk van: `EPIC-02` ## Definition of done Check-in kan mobiel comfortabel worden ingevuld. Backlog high Ochtendcheck-in release:r1, epic:ochtendcheckin, type:ui 2026-04-17T00:00:00Z Release 1 MVP
12 Inspannings Monitor Server action voor createMorningCheckIn Implementeer de server action voor het opslaan van de ochtendcheck-in. ## Context - Bron backlog-ID: `ST-202` - Epic / project: `Ochtendcheck-in` - Fase: `Release 1` - Afhankelijk van: `ST-201` ## Definition of done Check-in wordt opgeslagen met juiste validatie. Backlog high Ochtendcheck-in release:r1, epic:ochtendcheckin, type:build 2026-04-17T00:00:00Z Release 1 MVP
13 Inspannings Monitor Budgetlogica implementeren Bouw mapping van score naar energy level en dagbudget. ## Context - Bron backlog-ID: `ST-203` - Epic / project: `Ochtendcheck-in` - Fase: `Release 1` - Afhankelijk van: `ST-202` ## Definition of done Score mapping en budgetberekening zijn consistent en testbaar. Backlog high Ochtendcheck-in release:r1, epic:ochtendcheckin, type:logic 2026-04-17T00:00:00Z Release 1 MVP
14 Inspannings Monitor Check-instatus op dashboard tonen Toon direct score, niveau en budget op het dashboard. ## Context - Bron backlog-ID: `ST-204` - Epic / project: `Ochtendcheck-in` - Fase: `Release 1` - Afhankelijk van: `ST-202` ## Definition of done Gebruiker ziet direct score, niveau en budget. Backlog high Ochtendcheck-in release:r1, epic:ochtendcheckin, type:ui 2026-04-17T00:00:00Z Release 1 MVP
15 Inspannings Monitor Unit tests voor score- en budgetmapping Voeg tests toe voor grenswaarden en budgetberekening. ## Context - Bron backlog-ID: `ST-205` - Epic / project: `Ochtendcheck-in` - Fase: `Release 1` - Afhankelijk van: `ST-203` ## Definition of done Belangrijkste grenswaarden zijn afgedekt. Backlog high Ochtendcheck-in release:r1, epic:ochtendcheckin, type:qa 2026-04-17T00:00:00Z Release 1 MVP
16 Inspannings Monitor Datamodel voor activiteiten implementeren Implementeer tabellen en seed-data voor activiteiten, categorieen en skip-redenen. ## Context - Bron backlog-ID: `ST-301` - Epic / project: `Dagplanning` - Fase: `Release 1` - Afhankelijk van: `EPIC-03` ## Definition of done Migraties en seed-data voor categorieen en skip-redenen zijn aanwezig. Backlog high Dagplanning release:r1, epic:dagplanning, type:build 2026-04-17T00:00:00Z Release 1 MVP
17 Inspannings Monitor Planningformulier bouwen Bouw het formulier voor naam, categorie, duur, impact en prioriteit. ## Context - Bron backlog-ID: `ST-302` - Epic / project: `Dagplanning` - Fase: `Release 1` - Afhankelijk van: `ST-301` ## Definition of done Activiteit kan met naam, categorie, duur, impact en prioriteit worden aangemaakt. Backlog high Dagplanning release:r1, epic:dagplanning, type:ui 2026-04-17T00:00:00Z Release 1 MVP
18 Inspannings Monitor Autocomplete op eerdere activiteiten toevoegen Maak snelle herselectie van eerder gebruikte activiteiten mogelijk. ## Context - Bron backlog-ID: `ST-303` - Epic / project: `Dagplanning` - Fase: `Release 1` - Afhankelijk van: `ST-302` ## Definition of done Veelgebruikte activiteiten zijn snel opnieuw te kiezen. Backlog high Dagplanning release:r1, epic:dagplanning, type:ux 2026-04-17T00:00:00Z Release 1 MVP
19 Inspannings Monitor EnergyMeter en lopend totaal implementeren Toon het lopende totaal ten opzichte van het dagbudget. ## Context - Bron backlog-ID: `ST-304` - Epic / project: `Dagplanning` - Fase: `Release 1` - Afhankelijk van: `ST-302` ## Definition of done Totaal update direct na elke wijziging. Backlog high Dagplanning release:r1, epic:dagplanning, type:logic-ui 2026-04-17T00:00:00Z Release 1 MVP
20 Inspannings Monitor Overschrijdingswaarschuwing toevoegen Toon een niet-blokkerende waarschuwing bij budgetoverschrijding. ## Context - Bron backlog-ID: `ST-305` - Epic / project: `Dagplanning` - Fase: `Release 1` - Afhankelijk van: `ST-304` ## Definition of done Gebruiker krijgt feedback maar behoudt regie. Backlog high Dagplanning release:r1, epic:dagplanning, type:ux 2026-04-17T00:00:00Z Release 1 MVP
21 Inspannings Monitor Statusflows voor uitgevoerd, geskipt en aangepast bouwen Implementeer de drie kernstatussen voor activiteiten. ## Context - Bron backlog-ID: `ST-401` - Epic / project: `Evaluatie en dagoverzicht` - Fase: `Release 1` - Afhankelijk van: `EPIC-04` ## Definition of done Alle drie de statussen worden correct opgeslagen. Backlog high Evaluatie en dagoverzicht release:r1, epic:evaluatie, type:build 2026-04-17T00:00:00Z Release 1 MVP
22 Inspannings Monitor Evaluatievelden toevoegen Voeg contextuele velden toe voor werkelijke duur, fatigue en skip-reden. ## Context - Bron backlog-ID: `ST-402` - Epic / project: `Evaluatie en dagoverzicht` - Fase: `Release 1` - Afhankelijk van: `ST-401` ## Definition of done Contextuele velden verschijnen passend per status. Backlog high Evaluatie en dagoverzicht release:r1, epic:evaluatie, type:ui 2026-04-17T00:00:00Z Release 1 MVP
23 Inspannings Monitor Ongeplande activiteiten ondersteunen Maak het mogelijk een ongeplande activiteit toe te voegen en mee te tellen. ## Context - Bron backlog-ID: `ST-403` - Epic / project: `Evaluatie en dagoverzicht` - Fase: `Release 1` - Afhankelijk van: `ST-401` ## Definition of done Ongeplande activiteit telt mee in werkelijke totalen. Backlog high Evaluatie en dagoverzicht release:r1, epic:evaluatie, type:build 2026-04-17T00:00:00Z Release 1 MVP
24 Inspannings Monitor Dagoverzicht bouwen Bouw het overzicht met gepland versus uitgevoerd en statusverdeling. ## Context - Bron backlog-ID: `ST-404` - Epic / project: `Evaluatie en dagoverzicht` - Fase: `Release 1` - Afhankelijk van: `ST-401` ## Definition of done Gepland versus uitgevoerd en statusverdeling zijn zichtbaar. Backlog high Evaluatie en dagoverzicht release:r1, epic:evaluatie, type:ui 2026-04-17T00:00:00Z Release 1 MVP
25 Inspannings Monitor Dagaggregaties server-side implementeren Bereken dagtotalen en samenvatting server-side. ## Context - Bron backlog-ID: `ST-405` - Epic / project: `Evaluatie en dagoverzicht` - Fase: `Release 1` - Afhankelijk van: `ST-404` ## Definition of done Dagtotalen blijven consistent met individuele records. Backlog high Evaluatie en dagoverzicht release:r1, epic:evaluatie, type:logic 2026-04-17T00:00:00Z Release 1 MVP
26 Inspannings Monitor Weekoverzichtspagina bouwen Bouw de pagina voor weekterugblik. ## Context - Bron backlog-ID: `ST-501` - Epic / project: `Weekoverzicht en inzichten` - Fase: `Release 1` - Afhankelijk van: `EPIC-05` ## Definition of done Gebruiker kan per week terugkijken. Backlog medium Weekoverzicht en inzichten release:r1, epic:weekoverzicht, type:ui 2026-04-17T00:00:00Z Release 1 MVP
27 Inspannings Monitor Weekaggregaties bouwen Bereken gemiddelde energie en budget-adherence per week. ## Context - Bron backlog-ID: `ST-502` - Epic / project: `Weekoverzicht en inzichten` - Fase: `Release 1` - Afhankelijk van: `ST-501` ## Definition of done Gemiddelde energie en budget-adherence zijn herleidbaar en testbaar. Backlog medium Weekoverzicht en inzichten release:r1, epic:weekoverzicht, type:logic 2026-04-17T00:00:00Z Release 1 MVP
28 Inspannings Monitor Skip-patronen zichtbaar maken Toon patronen rond skip-redenen en terugkerende activiteiten. ## Context - Bron backlog-ID: `ST-503` - Epic / project: `Weekoverzicht en inzichten` - Fase: `Release 1` - Afhankelijk van: `ST-502` ## Definition of done Patronen worden alleen bij voldoende data getoond. Backlog medium Weekoverzicht en inzichten release:r1, epic:weekoverzicht, type:logic-ui 2026-04-17T00:00:00Z Release 1 MVP
29 Inspannings Monitor Insightregels met datadrempels definiëren Leg guardrails vast voor het tonen van patronen. ## Context - Bron backlog-ID: `ST-504` - Epic / project: `Weekoverzicht en inzichten` - Fase: `Release 1` - Afhankelijk van: `ST-502` ## Definition of done Geen patroonclaim zonder guardrails. Backlog medium Weekoverzicht en inzichten release:r1, epic:weekoverzicht, type:safety-logic 2026-04-17T00:00:00Z Release 1 MVP
30 Inspannings Monitor Insightcopy toetsen op niet-medische formulering Controleer alle inzichtteksten op wellness-positionering. ## Context - Bron backlog-ID: `ST-505` - Epic / project: `Weekoverzicht en inzichten` - Fase: `Release 1` - Afhankelijk van: `ST-504` ## Definition of done Alle teksten blijven binnen wellness-positionering. Backlog medium Weekoverzicht en inzichten release:r1, epic:weekoverzicht, type:content 2026-04-17T00:00:00Z Release 1 MVP
31 Inspannings Monitor ReflectionCheckIn-model en flow implementeren Implementeer model en basisflow voor reflectie na een zwaardere dag. ## Context - Bron backlog-ID: `ST-601` - Epic / project: `Reflectie en reminders` - Fase: `Release 1` - Afhankelijk van: `EPIC-05` ## Definition of done Reflecties kunnen aan eerdere dagen gekoppeld worden. Backlog medium Reflectie en reminders release:r1, epic:reflectie, type:build 2026-04-17T00:00:00Z Release 1 MVP
32 Inspannings Monitor Joblogica voor T+1/T+2 prompts bouwen Bepaal server-side welke gebruikers een reflectieprompt moeten zien. ## Context - Bron backlog-ID: `ST-602` - Epic / project: `Reflectie en reminders` - Fase: `Release 1` - Afhankelijk van: `ST-601` ## Definition of done Prompts worden niet dubbel of willekeurig aangemaakt. Backlog medium Reflectie en reminders release:r1, epic:reflectie, type:logic-ops 2026-04-17T00:00:00Z Release 1 MVP
33 Inspannings Monitor Instellingsoptie voor reflectieprompts toevoegen Maak opt-in beheerbaar vanuit instellingen. ## Context - Bron backlog-ID: `ST-603` - Epic / project: `Reflectie en reminders` - Fase: `Release 1` - Afhankelijk van: `ST-104` ## Definition of done Gebruiker beheert opt-in zelfstandig. Backlog medium Reflectie en reminders release:r1, epic:reflectie, type:build 2026-04-17T00:00:00Z Release 1 MVP
34 Inspannings Monitor Korte reflectie-UI bouwen Bouw een lichte, niet-medische reflectieprompt. ## Context - Bron backlog-ID: `ST-604` - Epic / project: `Reflectie en reminders` - Fase: `Release 1` - Afhankelijk van: `ST-602` ## Definition of done Prompt voelt licht en niet medisch. Backlog medium Reflectie en reminders release:r1, epic:reflectie, type:ui 2026-04-17T00:00:00Z Release 1 MVP
35 Inspannings Monitor Rate limiting toevoegen Bescherm kritieke auth- en mutatieroutes tegen misbruik. ## Context - Bron backlog-ID: `ST-701` - Epic / project: `Security en operations` - Fase: `Release 1` - Afhankelijk van: `EPIC-02` ## Definition of done Kritieke auth- en mutatieroutes zijn beschermd. Backlog high Security en operations release:r1, epic:security-ops, type:security 2026-04-17T00:00:00Z Release 1 MVP
36 Inspannings Monitor Logging voor fouten en kernmutaties inrichten Log fouten, loginproblemen en belangrijke mutaties centraal. ## Context - Bron backlog-ID: `ST-702` - Epic / project: `Security en operations` - Fase: `Release 1` - Afhankelijk van: `EPIC-03,EPIC-04,EPIC-05` ## Definition of done Kerngebeurtenissen zijn herleidbaar. Backlog high Security en operations release:r1, epic:security-ops, type:ops 2026-04-17T00:00:00Z Release 1 MVP
37 Inspannings Monitor Back-up en herstelstrategie documenteren en testen Werk het restore-pad uit en valideer het. ## Context - Bron backlog-ID: `ST-703` - Epic / project: `Security en operations` - Fase: `Release 1` - Afhankelijk van: `EPIC-01` ## Definition of done Restore-pad is aantoonbaar gevalideerd. Backlog high Security en operations release:r1, epic:security-ops, type:ops 2026-04-17T00:00:00Z Release 1 MVP
38 Inspannings Monitor Secrets- en environmentbeheer formaliseren Leg veilig beheer van secrets en omgevingen vast voor Vercel en Supabase. ## Context - Bron backlog-ID: `ST-704` - Epic / project: `Security en operations` - Fase: `Release 1` - Afhankelijk van: `EPIC-01` ## Definition of done Geen secrets in code of onveilige configuratie. Backlog high Security en operations release:r1, epic:security-ops, type:security-ops 2026-04-17T00:00:00Z Release 1 MVP
39 Inspannings Monitor RLS-policy tests toevoegen Test aantoonbaar dat owner-only toegang technisch afgedwongen is. ## Context - Bron backlog-ID: `ST-705` - Epic / project: `Security en operations` - Fase: `Release 1` - Afhankelijk van: `ST-105` ## Definition of done Owner-only model is aantoonbaar afgedwongen. Backlog high Security en operations release:r1, epic:security-ops, type:qa-security 2026-04-17T00:00:00Z Release 1 MVP
40 Inspannings Monitor Kernflows handmatig testen Voer end-to-end handmatige tests uit op mobiel en desktop. ## Context - Bron backlog-ID: `ST-801` - Epic / project: `Launch-readiness` - Fase: `Release 1` - Afhankelijk van: `EPIC-05,EPIC-06,EPIC-07` ## Definition of done Belangrijkste user journeys zijn geverifieerd. Backlog high Launch-readiness release:r1, epic:launch, type:qa 2026-04-17T00:00:00Z Release 1 MVP
41 Inspannings Monitor Accessibility check uitvoeren Controleer touch targets, contrast en reduced motion. ## Context - Bron backlog-ID: `ST-802` - Epic / project: `Launch-readiness` - Fase: `Release 1` - Afhankelijk van: `EPIC-05` ## Definition of done Touch targets, contrast en reduced motion zijn gecontroleerd. Backlog high Launch-readiness release:r1, epic:launch, type:qa-ux 2026-04-17T00:00:00Z Release 1 MVP
42 Inspannings Monitor Copy review doen Controleer onboarding, dashboardteksten en inzichten op wellness-copy. ## Context - Bron backlog-ID: `ST-803` - Epic / project: `Launch-readiness` - Fase: `Release 1` - Afhankelijk van: `EPIC-06` ## Definition of done Geen medische of zorgdossier-taal in release 1. Backlog high Launch-readiness release:r1, epic:launch, type:content-safety 2026-04-17T00:00:00Z Release 1 MVP
43 Inspannings Monitor DPIA-input en datacatalogus afronden Rond privacyartefacten af op basis van de werkelijke MVP-scope. ## Context - Bron backlog-ID: `ST-804` - Epic / project: `Launch-readiness` - Fase: `Release 1` - Afhankelijk van: `EPIC-08` ## Definition of done Pre-launch privacyartefacten zijn gereed. Backlog high Launch-readiness release:r1, epic:launch, type:privacy 2026-04-17T00:00:00Z Release 1 MVP
44 Inspannings Monitor Go-live checklist opstellen Maak een checklist voor launch, rollback, monitoring en incidentrespons. ## Context - Bron backlog-ID: `ST-805` - Epic / project: `Launch-readiness` - Fase: `Release 1` - Afhankelijk van: `EPIC-08` ## Definition of done Team weet hoe launch en eerste incidentrespons verloopt. Backlog high Launch-readiness release:r1, epic:launch, type:ops 2026-04-17T00:00:00Z Release 1 MVP

View file

@ -0,0 +1,10 @@
Name,Summary,Status,Milestones,Creator,Lead,Members,Created At,Started At,Target Date,Completed At,Canceled At,Teams,Initiatives
Fundament,"Leg de technische basis voor release 1 met projectsetup, omgevingen, UI-basis en foutafhandeling.",Planned,,,,,2026-04-17T00:00:00Z,,,,,Inspannings Monitor,Release 1 MVP
Auth en profiel,"Implementeer accounttoegang, profieldata, onboarding en basisinstellingen per gebruiker.",Planned,,,,,2026-04-17T00:00:00Z,,,,,Inspannings Monitor,Release 1 MVP
Ochtendcheck-in,"Bouw de ochtendcheck-in met energiescore, slaapkwaliteit en automatische budgetafleiding.",Planned,,,,,2026-04-17T00:00:00Z,,,,,Inspannings Monitor,Release 1 MVP
Dagplanning,"Maak plannen van activiteiten mogelijk met budgetfeedback, energie-impact en prioriteit.",Planned,,,,,2026-04-17T00:00:00Z,,,,,Inspannings Monitor,Release 1 MVP
Evaluatie en dagoverzicht,Maak evaluatie van activiteiten en een dagelijks overzicht van gepland versus uitgevoerd mogelijk.,Planned,,,,,2026-04-17T00:00:00Z,,,,,Inspannings Monitor,Release 1 MVP
Weekoverzicht en inzichten,"Voeg weekterugblik, eenvoudige aggregaties en veilige patroonweergave toe zonder medische claims.",Backlog,,,,,2026-04-17T00:00:00Z,,,,,Inspannings Monitor,Release 1 MVP
Reflectie en reminders,Voeg optionele T+1/T+2 reflectieprompts en lichte reminderlogica toe voor zwaardere dagen.,Backlog,,,,,2026-04-17T00:00:00Z,,,,,Inspannings Monitor,Release 1 MVP
Security en operations,"Borg logging, rate limiting, secrets, back-up en owner-only toegangscontrole voor echte gebruikersintroductie.",Planned,,,,,2026-04-17T00:00:00Z,,,,,Inspannings Monitor,Release 1 MVP
Launch-readiness,"Rond QA, copy review, accessibility checks, DPIA-input en go-live checks voor release 1 af.",Backlog,,,,,2026-04-17T00:00:00Z,,,,,Inspannings Monitor,Release 1 MVP
1 Name Summary Status Milestones Creator Lead Members Created At Started At Target Date Completed At Canceled At Teams Initiatives
2 Fundament Leg de technische basis voor release 1 met projectsetup, omgevingen, UI-basis en foutafhandeling. Planned 2026-04-17T00:00:00Z Inspannings Monitor Release 1 MVP
3 Auth en profiel Implementeer accounttoegang, profieldata, onboarding en basisinstellingen per gebruiker. Planned 2026-04-17T00:00:00Z Inspannings Monitor Release 1 MVP
4 Ochtendcheck-in Bouw de ochtendcheck-in met energiescore, slaapkwaliteit en automatische budgetafleiding. Planned 2026-04-17T00:00:00Z Inspannings Monitor Release 1 MVP
5 Dagplanning Maak plannen van activiteiten mogelijk met budgetfeedback, energie-impact en prioriteit. Planned 2026-04-17T00:00:00Z Inspannings Monitor Release 1 MVP
6 Evaluatie en dagoverzicht Maak evaluatie van activiteiten en een dagelijks overzicht van gepland versus uitgevoerd mogelijk. Planned 2026-04-17T00:00:00Z Inspannings Monitor Release 1 MVP
7 Weekoverzicht en inzichten Voeg weekterugblik, eenvoudige aggregaties en veilige patroonweergave toe zonder medische claims. Backlog 2026-04-17T00:00:00Z Inspannings Monitor Release 1 MVP
8 Reflectie en reminders Voeg optionele T+1/T+2 reflectieprompts en lichte reminderlogica toe voor zwaardere dagen. Backlog 2026-04-17T00:00:00Z Inspannings Monitor Release 1 MVP
9 Security en operations Borg logging, rate limiting, secrets, back-up en owner-only toegangscontrole voor echte gebruikersintroductie. Planned 2026-04-17T00:00:00Z Inspannings Monitor Release 1 MVP
10 Launch-readiness Rond QA, copy review, accessibility checks, DPIA-input en go-live checks voor release 1 af. Backlog 2026-04-17T00:00:00Z Inspannings Monitor Release 1 MVP

View file

@ -0,0 +1,121 @@
# Inspannings Monitor Linear Projects
Dit document geeft per `Linear Project` een aanbevolen naam, samenvatting, status en praktisch gebruik.
## Initiative
### Release 1 MVP
- Aanbevolen status: `Planned`
- Samenvatting: `Wellness-first MVP voor individuele gebruikers, met een lichte plan-doe-evalueer flow voor energiemanagement.`
- Doel: alle release-1 projecten samenbrengen onder één duidelijk productdoel
Volgens de actuele `Linear`-documentatie zijn initiatives workspace-breed, bedoeld om projecten te groeperen rond een organisatorisch doel, en hebben ze een lifecycle met `Planned`, `Active` en `Completed`. Voor jullie huidige fase is `Planned` de juiste startstatus.
## Projects
### 1. Fundament
- Aanbevolen status: `Planned`
- Samenvatting: `Leg de technische basis voor release 1 met projectsetup, omgevingen, UI-basis en foutafhandeling.`
- Waarom dit een project is:
- duidelijke uitkomst
- vroeg in de planning
- direct blokkerend voor alle andere projecten
### 2. Auth en profiel
- Aanbevolen status: `Planned`
- Samenvatting: `Implementeer accounttoegang, profieldata, onboarding en basisinstellingen per gebruiker.`
- Waarom dit een project is:
- eigen domein met duidelijke oplevering
- nodig voor alle persoonlijke flows
### 3. Ochtendcheck-in
- Aanbevolen status: `Planned`
- Samenvatting: `Bouw de ochtendcheck-in met energiescore, slaapkwaliteit en automatische budgetafleiding.`
- Waarom dit een project is:
- centrale start van de kerngebruikersreis
- duidelijke functionele grens
### 4. Dagplanning
- Aanbevolen status: `Planned`
- Samenvatting: `Maak plannen van activiteiten mogelijk met budgetfeedback, energie-impact en prioriteit.`
- Waarom dit een project is:
- aparte UX- en datamodelscope
- kern van de planfase
### 5. Evaluatie en dagoverzicht
- Aanbevolen status: `Planned`
- Samenvatting: `Maak evaluatie van activiteiten en een dagelijks overzicht van gepland versus uitgevoerd mogelijk.`
- Waarom dit een project is:
- sluit de kernloop functioneel af
- levert directe gebruikerswaarde op
### 6. Weekoverzicht en inzichten
- Aanbevolen status: `Backlog`
- Samenvatting: `Voeg weekterugblik, eenvoudige aggregaties en veilige patroonweergave toe zonder medische claims.`
- Waarom dit een project is:
- logisch vervolg op de basisflow
- minder blokkerend dan de eerste vijf projecten
### 7. Reflectie en reminders
- Aanbevolen status: `Backlog`
- Samenvatting: `Voeg optionele T+1/T+2 reflectieprompts en lichte reminderlogica toe voor zwaardere dagen.`
- Waarom dit een project is:
- waardevol, maar niet nodig om de eerste basisflow werkend te krijgen
- goed af te bakenen als apart project
### 8. Security en operations
- Aanbevolen status: `Planned`
- Samenvatting: `Borg logging, rate limiting, secrets, back-up en owner-only toegangscontrole voor echte gebruikersintroductie.`
- Waarom dit een project is:
- releasekritisch
- loopt parallel aan featurebouw
### 9. Launch-readiness
- Aanbevolen status: `Backlog`
- Samenvatting: `Rond QA, copy review, accessibility checks, DPIA-input en go-live checks voor release 1 af.`
- Waarom dit een project is:
- hoort als apart releaseproject zichtbaar te zijn
- wordt pas later actief, maar moet wel vroeg bestaan
## Aanbevolen praktische werkwijze in Linear
- Gebruik `Projects` voor deze 9 grotere werkstromen.
- Gebruik `Issues` voor de individuele stories.
- Gebruik voorlopig geen milestones.
- Gebruik voorlopig geen cycles.
- Zet een project pas op `In Progress` zodra er daadwerkelijk actief werk in loopt.
- Laat `Weekoverzicht en inzichten`, `Reflectie en reminders` en `Launch-readiness` aanvankelijk op `Backlog` staan.
## Aanbevolen eerste statusverdeling
### Start op `Planned`
- `Fundament`
- `Auth en profiel`
- `Ochtendcheck-in`
- `Dagplanning`
- `Evaluatie en dagoverzicht`
- `Security en operations`
### Start op `Backlog`
- `Weekoverzicht en inzichten`
- `Reflectie en reminders`
- `Launch-readiness`
## Bronnen
- [Linear Project status](https://linear.app/docs/project-status)
- [Linear Projects](https://linear.app/docs/projects)
- [Linear Project overview](https://linear.app/docs/project-overview)
- [Linear Initiatives](https://linear.app/docs/initiatives)

View file

@ -0,0 +1,112 @@
# Inspannings Monitor in Linear
Dit document vertaalt de huidige backlog en documentatieset naar een praktische `Linear`-inrichting.
## Waarom deze inrichting
Volgens de actuele `Linear`-documentatie is een workspace het hoogste niveau en beveelt Linear in de praktijk aan om per bedrijf één workspace te gebruiken. Ook is een issue in Linear altijd gekoppeld aan precies één team, terwijl projecten grotere eenheden van werk zijn met een duidelijke uitkomst en geplande afronding.
Voor `Inspannings Monitor` betekent dat: houd het in het begin eenvoudig en maak één team voor release 1.
## Aanbevolen structuur
### Workspace
- `Inspannings Monitor`
### Team
- `Inspannings Monitor`
Gebruik één top-level team voor release 1. Dat past goed bij het feit dat release 1 alleen voor individuele gebruikers is en dat de backlog nog niet over meerdere product- of engineeringteams verdeeld hoeft te worden.
### Initiative
- `Release 1 MVP`
Gebruik één initiative als overkoepelend kader voor de eerste release.
### Projects
Maak de huidige epics als `Projects` aan in Linear:
1. `Fundament`
2. `Auth en profiel`
3. `Ochtendcheck-in`
4. `Dagplanning`
5. `Evaluatie en dagoverzicht`
6. `Weekoverzicht en inzichten`
7. `Reflectie en reminders`
8. `Security en operations`
9. `Launch-readiness`
## Aanbevolen labels
Houd labels klein en functioneel:
- `release:r1`
- `type:build`
- `type:ui`
- `type:logic`
- `type:qa`
- `type:security`
- `type:ops`
- `type:ux`
- `type:content`
- `type:privacy`
- `epic:fundament`
- `epic:auth-profiel`
- `epic:ochtendcheckin`
- `epic:dagplanning`
- `epic:evaluatie`
- `epic:weekoverzicht`
- `epic:reflectie`
- `epic:security-ops`
- `epic:launch`
## Aanbevolen statusgebruik
Voor de eerste release zou ik de workflow simpel houden:
- `Backlog`
- `Todo`
- `In Progress`
- `Done`
- `Canceled`
Begin zonder extra workflowstappen zoals `In Review`, tenzij jullie daar direct echt behoefte aan hebben. Linear is sterk juist wanneer je niet te vroeg te veel proceslagen toevoegt.
## Hoe ik de backlog heb gemapt
- `Epics` uit onze backlog zijn gemapt naar `Projects` in Linear.
- `Stories` zijn gemapt naar `Issues`.
- `P0` is gemapt naar `high`.
- `P1` is gemapt naar `medium`.
- Alle issues starten in `Backlog`.
- Het labelpakket uit de bestaande backlog blijft behouden.
## Aanbevolen importaanpak
1. Maak in Linear eerst de workspace en het team aan.
2. Maak daarna handmatig de `Initiative` en de negen `Projects` aan.
3. Gebruik het gegenereerde bestand [inspannings-monitor-linear-import-issues.csv](./inspannings-monitor-linear-import-issues.csv).
4. Gebruik de importroute die Linear documenteert voor `Other`-bronnen / `Linear CSV` via hun importer/CLI.
5. Controleer na import of `Project` en `Initiatives` goed zijn overgekomen.
6. Als die velden niet automatisch gekoppeld blijken, kun je in Linear issues per `epic:*` label filteren en daarna in bulk aan het juiste project koppelen.
## Belangrijke noot over de CSV
De gegenereerde CSV volgt de actuele exportkoppen van Linear, zodat het formaat dicht op het eigen model van Linear ligt. De importdocumentatie noemt expliciet onder meer `Title`, `Description`, `Priority`, `Status`, `Assignee`, `Created`, `Completed`, `Labels` en `Estimate` als relevante velden voor een `Other`-import. Ik heb daarnaast ook `Team`, `Project` en `Initiatives` ingevuld op basis van Linears eigen exportstructuur. Daardoor is de CSV zo bruikbaar mogelijk, maar het blijft verstandig om na import even te verifiëren dat projectkoppelingen exact zijn overgekomen.
## Cycles
Ik zou `Cycles` nog niet meteen aanzetten. Eerst de basisflow goed krijgen, daarna pas time-boxing toevoegen. Linear ondersteunt cycles per team, maar voor deze eerste release levert een eenvoudige project- en issue-structuur waarschijnlijk meer rust op dan direct sprintdiscipline.
## Bestanden in deze map
- [inspannings-monitor-backlog.md](./inspannings-monitor-backlog.md)
- [inspannings-monitor-backlog.csv](./inspannings-monitor-backlog.csv)
- [inspannings-monitor-linear-import-issues.csv](./inspannings-monitor-linear-import-issues.csv)
- [inspannings-monitor-linear-projects.csv](./inspannings-monitor-linear-projects.csv)
- [inspannings-monitor-linear-projects.md](./inspannings-monitor-linear-projects.md)
- [generate_linear_backlog_assets.py](./generate_linear_backlog_assets.py)

File diff suppressed because it is too large Load diff

Binary file not shown.

5
eslint.config.mjs Normal file
View file

@ -0,0 +1,5 @@
import nextVitals from "eslint-config-next/core-web-vitals";
const eslintConfig = [...nextVitals];
export default eslintConfig;

71
lib/auth/messages.ts Normal file
View file

@ -0,0 +1,71 @@
export type AuthNoticeTone = "error" | "success" | "info";
export type AuthNotice = {
tone: AuthNoticeTone;
text: string;
};
const errorMessages: Record<string, AuthNotice> = {
"auth-not-configured": {
tone: "error",
text: "Supabase is nog niet geconfigureerd. Voeg eerst je URL en publishable key toe.",
},
"invalid-credentials": {
tone: "error",
text: "De combinatie van e-mailadres en wachtwoord klopt niet.",
},
"email-not-confirmed": {
tone: "error",
text: "Bevestig eerst je e-mailadres via de link in je inbox.",
},
"missing-fields": {
tone: "error",
text: "Vul zowel je e-mailadres als je wachtwoord in.",
},
"signup-failed": {
tone: "error",
text: "Je account kon niet worden aangemaakt. Probeer het opnieuw.",
},
"signup-rate-limited": {
tone: "error",
text: "Er zijn nu te veel verificatie-e-mails verstuurd. Wacht even en probeer het daarna opnieuw.",
},
"login-failed": {
tone: "error",
text: "Inloggen is niet gelukt. Probeer het opnieuw.",
},
"verification-failed": {
tone: "error",
text: "De verificatielink is ongeldig of verlopen. Vraag zo nodig een nieuwe aan.",
},
};
const statusMessages: Record<string, AuthNotice> = {
"check-email": {
tone: "success",
text: "Controleer je e-mail en activeer je account via de verificatielink.",
},
"signed-out": {
tone: "info",
text: "Je bent uitgelogd.",
},
verified: {
tone: "success",
text: "Je e-mailadres is bevestigd. Welkom terug.",
},
};
export function getAuthNotice(
error?: string | null,
status?: string | null,
): AuthNotice | null {
if (error && errorMessages[error]) {
return errorMessages[error];
}
if (status && statusMessages[status]) {
return statusMessages[status];
}
return null;
}

41
lib/auth/navigation.ts Normal file
View file

@ -0,0 +1,41 @@
const DEFAULT_NEXT_PATH = "/dashboard";
export function sanitizeNextPath(candidate?: string | null): string {
if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//")) {
return DEFAULT_NEXT_PATH;
}
return candidate;
}
export function buildPathWithQuery(
pathname: string,
params: Record<string, string | null | undefined>,
): string {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value) {
search.set(key, value);
}
}
const query = search.toString();
return query ? `${pathname}?${query}` : pathname;
}
export function getRequestOrigin(headers: Pick<Headers, "get">): string {
const origin = headers.get("origin");
if (origin) {
return origin;
}
const protocol = headers.get("x-forwarded-proto") ?? "http";
const host = headers.get("x-forwarded-host") ?? headers.get("host");
if (host) {
return `${protocol}://${host}`;
}
return "http://localhost:3000";
}

64
lib/auth/session.ts Normal file
View file

@ -0,0 +1,64 @@
import { createClient } from "@/lib/supabase/server";
import { hasSupabaseEnv } from "@/lib/supabase/config";
type ClaimsShape = {
sub?: string;
email?: string;
};
export type AuthenticatedUser = {
id: string;
email: string | null;
};
export type AuthState = {
isConfigured: boolean;
isAuthenticated: boolean;
userId: string | null;
email: string | null;
};
export async function getAuthenticatedUser(): Promise<AuthenticatedUser | null> {
if (!hasSupabaseEnv()) {
return null;
}
const supabase = await createClient();
const { data } = await supabase.auth.getClaims();
if (!data?.claims) {
return null;
}
const claims = data.claims as ClaimsShape;
const userId = typeof claims.sub === "string" ? claims.sub : null;
if (!userId) {
return null;
}
return {
id: userId,
email: typeof claims.email === "string" ? claims.email : null,
};
}
export async function getAuthState(): Promise<AuthState> {
if (!hasSupabaseEnv()) {
return {
isConfigured: false,
isAuthenticated: false,
userId: null,
email: null,
};
}
const authenticatedUser = await getAuthenticatedUser();
return {
isConfigured: true,
isAuthenticated: Boolean(authenticatedUser),
userId: authenticatedUser?.id ?? null,
email: authenticatedUser?.email ?? null,
};
}

28
lib/onboarding/options.ts Normal file
View file

@ -0,0 +1,28 @@
export const ONBOARDING_TIMEZONE_OPTIONS = [
{
value: "Europe/Amsterdam",
label: "Europa/Amsterdam",
},
{
value: "Europe/Brussels",
label: "Europa/Brussel",
},
{
value: "Europe/Berlin",
label: "Europa/Berlijn",
},
{
value: "UTC",
label: "UTC",
},
] as const;
const ONBOARDING_TIMEZONE_SET = new Set(
ONBOARDING_TIMEZONE_OPTIONS.map((option) => option.value),
);
export function isSupportedOnboardingTimezone(value: string) {
return ONBOARDING_TIMEZONE_SET.has(
value as (typeof ONBOARDING_TIMEZONE_OPTIONS)[number]["value"],
);
}

385
lib/profile/service.ts Normal file
View file

@ -0,0 +1,385 @@
import { getAuthenticatedUser } from "@/lib/auth/session";
import { isSupportedOnboardingTimezone } from "@/lib/onboarding/options";
import { createClient } from "@/lib/supabase/server";
import type {
OnboardingSubmission,
ProfileBundle,
ProfileRecord,
SettingsSubmission,
UserSettingsRecord,
} from "@/lib/profile/types";
type SupabaseServerClient = Awaited<ReturnType<typeof createClient>>;
type ProfileRow = {
id: string;
email: string | null;
display_name: string | null;
locale: string;
timezone: string;
onboarding_seen: boolean;
onboarding_completed: boolean;
created_at: string;
updated_at: string;
};
type UserSettingsRow = {
profile_id: string;
morning_reminder_enabled: boolean;
morning_reminder_time: string | null;
reflection_reminder_enabled: boolean;
show_energy_points: boolean;
created_at: string;
updated_at: string;
};
type ProfileInsert = {
id: string;
email: string | null;
display_name: string | null;
locale: string;
timezone: string;
onboarding_seen: boolean;
onboarding_completed: boolean;
};
type UserSettingsInsert = {
profile_id: string;
morning_reminder_enabled: boolean;
morning_reminder_time: string | null;
reflection_reminder_enabled: boolean;
show_energy_points: boolean;
};
const PROFILE_COLUMNS =
"id, email, display_name, locale, timezone, onboarding_seen, onboarding_completed, created_at, updated_at";
const USER_SETTINGS_COLUMNS =
"profile_id, morning_reminder_enabled, morning_reminder_time, reflection_reminder_enabled, show_energy_points, created_at, updated_at";
const DEFAULT_LOCALE = "nl-NL";
const DEFAULT_TIMEZONE = "Europe/Amsterdam";
const SUPPORTED_LOCALES = new Set([DEFAULT_LOCALE]);
function mapProfileRow(row: ProfileRow): ProfileRecord {
return {
id: row.id,
email: row.email,
displayName: row.display_name,
locale: row.locale,
timezone: row.timezone,
onboardingSeen: row.onboarding_seen,
onboardingCompleted: row.onboarding_completed,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
function mapUserSettingsRow(row: UserSettingsRow): UserSettingsRecord {
return {
profileId: row.profile_id,
morningReminderEnabled: row.morning_reminder_enabled,
morningReminderTime: row.morning_reminder_time,
reflectionReminderEnabled: row.reflection_reminder_enabled,
showEnergyPoints: row.show_energy_points,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
export function buildDefaultProfileFromClaims(user: {
id: string;
email: string | null;
}): ProfileInsert {
return {
id: user.id,
email: user.email,
display_name: null,
locale: DEFAULT_LOCALE,
timezone: DEFAULT_TIMEZONE,
onboarding_seen: false,
onboarding_completed: false,
};
}
export function buildDefaultSettings(profileId: string): UserSettingsInsert {
return {
profile_id: profileId,
morning_reminder_enabled: false,
morning_reminder_time: null,
reflection_reminder_enabled: false,
show_energy_points: true,
};
}
function normalizeDisplayName(value: string | null) {
const trimmedValue = value?.trim() ?? "";
return trimmedValue ? trimmedValue : null;
}
function normalizeReminderTime(value: string | null, enabled: boolean) {
if (!enabled) {
return null;
}
const trimmedValue = value?.trim() ?? "";
return trimmedValue || "08:30";
}
function normalizeLocale(value: string) {
return SUPPORTED_LOCALES.has(value) ? value : DEFAULT_LOCALE;
}
function resolveTimezone(value: string) {
return isSupportedOnboardingTimezone(value) ? value : DEFAULT_TIMEZONE;
}
async function readProfileRow(
supabase: SupabaseServerClient,
userId: string,
): Promise<ProfileRow | null> {
const { data, error } = await supabase
.from("profiles")
.select(PROFILE_COLUMNS)
.eq("id", userId)
.maybeSingle();
if (error) {
throw new Error(`Profiel kon niet worden geladen: ${error.message}`);
}
return data;
}
async function readUserSettingsRow(
supabase: SupabaseServerClient,
userId: string,
): Promise<UserSettingsRow | null> {
const { data, error } = await supabase
.from("user_settings")
.select(USER_SETTINGS_COLUMNS)
.eq("profile_id", userId)
.maybeSingle();
if (error) {
throw new Error(`Instellingen konden niet worden geladen: ${error.message}`);
}
return data;
}
async function insertMissingProfile(
supabase: SupabaseServerClient,
user: { id: string; email: string | null },
) {
const { error } = await supabase.from("profiles").upsert(
buildDefaultProfileFromClaims(user),
{
onConflict: "id",
ignoreDuplicates: true,
},
);
if (error) {
throw new Error(`Profiel kon niet worden aangemaakt: ${error.message}`);
}
}
async function syncProfileEmailIfNeeded(
supabase: SupabaseServerClient,
profile: ProfileRow,
email: string | null,
): Promise<ProfileRow> {
if (!email || profile.email === email) {
return profile;
}
const { data, error } = await supabase
.from("profiles")
.update({ email })
.eq("id", profile.id)
.select(PROFILE_COLUMNS)
.single();
if (error) {
throw new Error(`Profiel-e-mailadres kon niet worden bijgewerkt: ${error.message}`);
}
return data;
}
async function insertMissingUserSettings(
supabase: SupabaseServerClient,
userId: string,
) {
const { error } = await supabase.from("user_settings").upsert(
buildDefaultSettings(userId),
{
onConflict: "profile_id",
ignoreDuplicates: true,
},
);
if (error) {
throw new Error(`Instellingen konden niet worden aangemaakt: ${error.message}`);
}
}
export async function markOnboardingSeenForCurrentUser() {
const user = await getAuthenticatedUser();
if (!user) {
throw new Error("Er is geen ingelogde gebruiker beschikbaar.");
}
const supabase = await createClient();
const { error } = await supabase
.from("profiles")
.update({ onboarding_seen: true, onboarding_completed: false })
.eq("id", user.id);
if (error) {
throw new Error(`Onboardingstatus kon niet worden bijgewerkt: ${error.message}`);
}
}
export async function completeOnboardingForCurrentUser(
submission: OnboardingSubmission,
) {
const user = await getAuthenticatedUser();
if (!user) {
throw new Error("Er is geen ingelogde gebruiker beschikbaar.");
}
const timezone = resolveTimezone(submission.timezone);
const supabase = await createClient();
const displayName = normalizeDisplayName(submission.displayName);
const morningReminderTime = normalizeReminderTime(
submission.morningReminderTime,
submission.morningReminderEnabled,
);
const { error: profileError } = await supabase
.from("profiles")
.update({
display_name: displayName,
timezone,
onboarding_seen: true,
onboarding_completed: true,
})
.eq("id", user.id);
if (profileError) {
throw new Error(`Profiel kon niet worden bijgewerkt: ${profileError.message}`);
}
const { error: settingsError } = await supabase
.from("user_settings")
.update({
morning_reminder_enabled: submission.morningReminderEnabled,
morning_reminder_time: morningReminderTime,
reflection_reminder_enabled: submission.reflectionReminderEnabled,
show_energy_points: submission.showEnergyPoints,
})
.eq("profile_id", user.id);
if (settingsError) {
throw new Error(`Instellingen konden niet worden bijgewerkt: ${settingsError.message}`);
}
return ensureProfileBundleForCurrentUser();
}
export async function saveSettingsForCurrentUser(
submission: SettingsSubmission,
) {
const user = await getAuthenticatedUser();
if (!user) {
throw new Error("Er is geen ingelogde gebruiker beschikbaar.");
}
await ensureProfileBundleForCurrentUser();
const supabase = await createClient();
const locale = normalizeLocale(submission.locale);
const timezone = resolveTimezone(submission.timezone);
const morningReminderTime = normalizeReminderTime(
submission.morningReminderTime,
submission.morningReminderEnabled,
);
const { error: profileError } = await supabase
.from("profiles")
.update({
locale,
timezone,
})
.eq("id", user.id);
if (profileError) {
throw new Error(`Profielinstellingen konden niet worden bijgewerkt: ${profileError.message}`);
}
const { error: settingsError } = await supabase
.from("user_settings")
.update({
morning_reminder_enabled: submission.morningReminderEnabled,
morning_reminder_time: morningReminderTime,
reflection_reminder_enabled: submission.reflectionReminderEnabled,
show_energy_points: submission.showEnergyPoints,
})
.eq("profile_id", user.id);
if (settingsError) {
throw new Error(`Gebruikersinstellingen konden niet worden bijgewerkt: ${settingsError.message}`);
}
return ensureProfileBundleForCurrentUser();
}
export async function ensureProfileBundleForCurrentUser(): Promise<ProfileBundle | null> {
const user = await getAuthenticatedUser();
if (!user) {
return null;
}
const supabase = await createClient();
// We bootstrap records app-side so the first protected page load is enough
// to give every authenticated user a minimal profile and settings basis.
let profileRow = await readProfileRow(supabase, user.id);
if (!profileRow) {
await insertMissingProfile(supabase, user);
profileRow = await readProfileRow(supabase, user.id);
}
if (!profileRow) {
throw new Error("Profielrecord ontbreekt na bootstrap.");
}
profileRow = await syncProfileEmailIfNeeded(supabase, profileRow, user.email);
let userSettingsRow = await readUserSettingsRow(supabase, user.id);
if (!userSettingsRow) {
await insertMissingUserSettings(supabase, user.id);
userSettingsRow = await readUserSettingsRow(supabase, user.id);
}
if (!userSettingsRow) {
throw new Error("Settingsrecord ontbreekt na bootstrap.");
}
return {
profile: mapProfileRow(profileRow),
settings: mapUserSettingsRow(userSettingsRow),
};
}
export async function getProfileBundleForCurrentUser(): Promise<ProfileBundle | null> {
return ensureProfileBundleForCurrentUser();
}

44
lib/profile/types.ts Normal file
View file

@ -0,0 +1,44 @@
export type ProfileRecord = {
id: string;
email: string | null;
displayName: string | null;
locale: string;
timezone: string;
onboardingSeen: boolean;
onboardingCompleted: boolean;
createdAt: string;
updatedAt: string;
};
export type UserSettingsRecord = {
profileId: string;
morningReminderEnabled: boolean;
morningReminderTime: string | null;
reflectionReminderEnabled: boolean;
showEnergyPoints: boolean;
createdAt: string;
updatedAt: string;
};
export type ProfileBundle = {
profile: ProfileRecord;
settings: UserSettingsRecord;
};
export type OnboardingSubmission = {
displayName: string | null;
timezone: string;
morningReminderEnabled: boolean;
morningReminderTime: string | null;
reflectionReminderEnabled: boolean;
showEnergyPoints: boolean;
};
export type SettingsSubmission = {
locale: string;
timezone: string;
morningReminderEnabled: boolean;
morningReminderTime: string | null;
reflectionReminderEnabled: boolean;
showEnergyPoints: boolean;
};

15
lib/supabase/client.ts Normal file
View file

@ -0,0 +1,15 @@
"use client";
import { createBrowserClient } from "@supabase/ssr";
import { getSupabaseEnv } from "@/lib/supabase/config";
let browserClient: ReturnType<typeof createBrowserClient> | undefined;
export function createClient() {
if (!browserClient) {
const { url, publishableKey } = getSupabaseEnv();
browserClient = createBrowserClient(url, publishableKey);
}
return browserClient;
}

22
lib/supabase/config.ts Normal file
View file

@ -0,0 +1,22 @@
export function hasSupabaseEnv(): boolean {
return Boolean(
process.env.NEXT_PUBLIC_SUPABASE_URL &&
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
);
}
export function getSupabaseEnv() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const publishableKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
if (!url || !publishableKey) {
throw new Error(
"Supabase configuratie ontbreekt. Voeg NEXT_PUBLIC_SUPABASE_URL en NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY toe.",
);
}
return {
url,
publishableKey,
};
}

40
lib/supabase/proxy.ts Normal file
View file

@ -0,0 +1,40 @@
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
import { getSupabaseEnv, hasSupabaseEnv } from "@/lib/supabase/config";
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({
request,
});
if (!hasSupabaseEnv()) {
return response;
}
const { url, publishableKey } = getSupabaseEnv();
const supabase = createServerClient(url, publishableKey, {
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => {
request.cookies.set(name, value);
});
response = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options);
});
},
},
});
await supabase.auth.getClaims();
return response;
}

26
lib/supabase/server.ts Normal file
View file

@ -0,0 +1,26 @@
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { getSupabaseEnv } from "@/lib/supabase/config";
export async function createClient() {
const cookieStore = await cookies();
const { url, publishableKey } = getSupabaseEnv();
return createServerClient(url, publishableKey, {
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
);
} catch {
// Server Components cannot always write cookies directly.
// The proxy keeps the session in sync for those cases.
}
},
},
});
}

6
lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

7
next.config.ts Normal file
View file

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
};
export default nextConfig;

10056
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

39
package.json Normal file
View file

@ -0,0 +1,39 @@
{
"name": "inspannings-monitor",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start",
"lint": "eslint ."
},
"dependencies": {
"@base-ui/react": "^1.4.0",
"@supabase/ssr": "^0.10.2",
"@supabase/supabase-js": "^2.103.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.8.0",
"next": "16.2.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"shadcn": "^4.3.0",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "latest",
"@types/node": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"eslint": "latest",
"eslint-config-next": "16.2.0",
"postcss": "latest",
"tailwindcss": "latest",
"typescript": "latest"
},
"engines": {
"node": ">=20.9.0"
}
}

7
postcss.config.mjs Normal file
View file

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

12
proxy.ts Normal file
View file

@ -0,0 +1,12 @@
import { type NextRequest } from "next/server";
import { updateSession } from "@/lib/supabase/proxy";
export async function proxy(request: NextRequest) {
return updateSession(request);
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};

View file

@ -0,0 +1 @@
v2.90.0

View file

@ -0,0 +1 @@
v2.188.1

View file

@ -0,0 +1 @@
{"ref":"yntzfgnkrwjlnbaxxkkc","name":"madhura68's Project","organization_id":"muclgkbblfugxcxrrjaj","organization_slug":"muclgkbblfugxcxrrjaj"}

View file

@ -0,0 +1 @@
postgresql://postgres.yntzfgnkrwjlnbaxxkkc@aws-0-eu-west-1.pooler.supabase.com:5432/postgres

View file

@ -0,0 +1 @@
17.6.1.104

View file

@ -0,0 +1 @@
yntzfgnkrwjlnbaxxkkc

View file

@ -0,0 +1 @@
v14.5

View file

@ -0,0 +1 @@
operation-ergonomics

View file

@ -0,0 +1 @@
v1.48.20

View file

@ -0,0 +1,7 @@
alter table public.profiles
add column if not exists onboarding_seen boolean not null default false;
update public.profiles
set onboarding_seen = true
where onboarding_completed = true
and onboarding_seen = false;

View file

@ -0,0 +1,93 @@
create or replace function public.set_updated_at()
returns trigger
language plpgsql
as $$
begin
new.updated_at = timezone('utc', now());
return new;
end;
$$;
create table if not exists public.profiles (
id uuid primary key references auth.users (id) on delete cascade,
email text,
display_name text,
locale text not null default 'nl-NL',
timezone text not null default 'Europe/Amsterdam',
onboarding_completed boolean not null default false,
created_at timestamptz not null default timezone('utc', now()),
updated_at timestamptz not null default timezone('utc', now())
);
create table if not exists public.user_settings (
profile_id uuid primary key references public.profiles (id) on delete cascade,
morning_reminder_enabled boolean not null default false,
morning_reminder_time time,
reflection_reminder_enabled boolean not null default false,
show_energy_points boolean not null default true,
created_at timestamptz not null default timezone('utc', now()),
updated_at timestamptz not null default timezone('utc', now())
);
grant usage on schema public to authenticated;
grant select, insert, update on table public.profiles to authenticated;
grant select, insert, update on table public.user_settings to authenticated;
alter table public.profiles enable row level security;
alter table public.user_settings enable row level security;
drop trigger if exists set_profiles_updated_at on public.profiles;
create trigger set_profiles_updated_at
before update on public.profiles
for each row
execute function public.set_updated_at();
drop trigger if exists set_user_settings_updated_at on public.user_settings;
create trigger set_user_settings_updated_at
before update on public.user_settings
for each row
execute function public.set_updated_at();
drop policy if exists "profiles_select_own" on public.profiles;
create policy "profiles_select_own"
on public.profiles
for select
to authenticated
using ((select auth.uid()) = id);
drop policy if exists "profiles_insert_own" on public.profiles;
create policy "profiles_insert_own"
on public.profiles
for insert
to authenticated
with check ((select auth.uid()) = id);
drop policy if exists "profiles_update_own" on public.profiles;
create policy "profiles_update_own"
on public.profiles
for update
to authenticated
using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);
drop policy if exists "user_settings_select_own" on public.user_settings;
create policy "user_settings_select_own"
on public.user_settings
for select
to authenticated
using ((select auth.uid()) = profile_id);
drop policy if exists "user_settings_insert_own" on public.user_settings;
create policy "user_settings_insert_own"
on public.user_settings
for insert
to authenticated
with check ((select auth.uid()) = profile_id);
drop policy if exists "user_settings_update_own" on public.user_settings;
create policy "user_settings_update_own"
on public.user_settings
for update
to authenticated
using ((select auth.uid()) = profile_id)
with check ((select auth.uid()) = profile_id);

41
tsconfig.json Normal file
View file

@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}