diff --git a/018_handicap_history.sql b/018_handicap_history.sql
new file mode 100644
index 0000000..998600f
--- /dev/null
+++ b/018_handicap_history.sql
@@ -0,0 +1,28 @@
+-- =====================================================================
+-- TeeCup — HCP-historikk over tid for personlig profil (migrasjon 018)
+-- =====================================================================
+-- Oppfølging av ADR-031 sitt "naturlig neste steg"-punkt: personlig
+-- profil sin `app_user.handicap_index` (ADR-031, migrasjon 015) endres i
+-- dag stille ved hver PATCH /auth/profile, uten noen logg over tidligere
+-- verdier. Denne migrasjonen legger til en append-only historikk-tabell
+-- -- IKKE noe erstatning for selve `app_user.handicap_index` (som
+-- fortsatt er "gjeldende verdi", brukt overalt ellers uendret).
+--
+-- Bevisst KUN for den PERSONLIGE profilens HCP -- IKKE de org-scopede
+-- `player.handicap_index`-radene eller `team_roster.handicap_index_
+-- snapshot` (som allerede har sitt eget, separate reproduserbarhets-
+-- prinsipp, ADR-007 -- ikke noe denne migrasjonen rører).
+-- =====================================================================
+
+\set ON_ERROR_STOP on
+
+CREATE TABLE handicap_history (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
+ handicap_index numeric(4,1) NOT NULL,
+ recorded_at timestamptz NOT NULL DEFAULT now()
+);
+
+CREATE INDEX ON handicap_history (user_id, recorded_at);
+
+GRANT SELECT, INSERT ON handicap_history TO teecup_app;
diff --git a/CLAUDE.md b/CLAUDE.md
index 4c0a542..59f783c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2150,10 +2150,39 @@ Ferdig og verifisert:
begge containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no`
upåvirket.
+- **Sekundær e-postadresse (del 1, det enkle tilfellet) — ✅ BYGGET OG LIVE
+ 2026-07-21**, samme dag, rett etter deltaker-tilgang-runden. Ny
+ migrasjon `017_secondary_email.sql` (`secondary_email_token` +
+ `user_secondary_email`, samme token-hash-og-utløp-mønster som ADR-032s
+ `email_change_token`). Nye endepunkter `POST /auth/secondary-email`,
+ `POST /auth/secondary-email/confirm`, `DELETE /auth/secondary-email/{id}`.
+ **Kjernestykket:** `verify_magic_link`/`login_with_password` slår nå opp
+ `user_secondary_email` FØR sitt vanlige `app_user.email`-oppslag — en
+ innlogging på en verifisert sekundæradresse løses til EIERENS
+ eksisterende konto i stedet for å opprette en ny, separat en (nøyaktig
+ det hullet som gjorde funksjonen nødvendig i utgangspunktet). Lagt i
+ `/account` (ikke dashbordet som opprinnelig bedt om — bevisst avvik,
+ flagget eksplisitt: dette er kun del 1, dashbord-plassering er trolig
+ riktigere når/hvis del 2 (kontosammenslåing) bygges).
+ **Scratch-verifisert, 20 sjekker:** adresse ikke lagt til før bekreftet,
+ token ikke gjenbrukbart, dupliserte adresser (som andres primær- ELLER
+ sekundæradresse) avvist tydelig, innlogging via sekundæradresse (magic-
+ link OG passord) bekreftet å resolve til SAMME eksisterende konto,
+ fremmed kan ikke slette andres adresse, fjernet adresse oppretter en
+ genuint NY konto ved neste innlogging (beviser fjerning er reell).
+ `test_isolation.sql` 12/12. Ekte typesjekket produksjonsbuild kjørt og
+ bekreftet.
+ **Rullet ut live 2026-07-21**, bruker bekreftet eksplisitt: migrasjon
+ 017 kjørt mot ekte `teecup_db` (begge tabeller bekreftet, `test_
+ isolation.sql` fortsatt 12/12), deretter `docker compose up -d --build
+ teecup_api teecup_frontend`. Begge containere boot-et rent,
+ `/health`/`/dashboard`/`/account`/`/verify-email` → 200, `teeoff.no`
+ upåvirket. Del 2 (ekte kontosammenslåing) fortsatt IKKE designet, egen
+ fremtidig runde, se FEATURE_BACKLOG.md.
+
Neste steg:
-1. **Pågår, samme økt:** bygge de to gjenværende bekreftede punktene fra
- dashboard/konto-runden (2026-07-21) — sekundær e-postadresse (enkelt
- tilfelle, gjenbruker ADR-032s token-mønster) og HCP-historikk over tid.
+1. **Pågår, samme økt:** HCP-historikk over tid — siste bekreftede punkt
+ fra dashboard/konto-runden (2026-07-21), gjenstår å bygge.
2. **Pauset, venter på retning:** dashbordets tom-tilstand ved første
innlogging. Brukeren ba om å justere det opprinnelige 2026-07-20-
forslaget i lys av et dypere spørsmål — bør organisasjon fortsatt være
@@ -2167,16 +2196,13 @@ Neste steg:
dedikert ADR-runde, IKKE noe som skal bygges i forlengelsen av en
vanlig funksjonsrunde. Se full analyse og åpne spørsmål i
FEATURE_BACKLOG.md.
-4. **Notert, IKKE designet i detalj:** én person med flere e-post-adresser
- — legge til en verifisert sekundær-adresse fra dashbordet, med
- turneringer/data som "dukker opp"/slås sammen deretter, og fremtidige
- innlogginger med enten adresse. Fanget grundig i FEATURE_BACKLOG.md,
- inkl. et reelt uløst spørsmål (ekte konto-sammenslåing hvis adressen
- allerede tilhører en annen eksisterende konto) som trenger egen,
- separat designrunde. **Merk:** dette overlapper med punkt 1 sin
- "sekundær e-postadresse" — punkt 1 er det ENKLE, avgrensede tilfellet
- (fri adresse), dette punktet er det harde tilfellet (adressen tilhører
- allerede en annen konto).
+4. **Del 1 (fri, ukrevd sekundær-e-post) er nå BYGGET OG LIVE** (2026-07-21,
+ se status over). **Del 2 (ekte konto-sammenslåing) fortsatt IKKE
+ designet:** hva skjer hvis den ønskede adressen ALLEREDE tilhører en
+ annen, eksisterende konto (i dag avvist tydelig med 409 DUPLICATE i
+ stedet for gjettet på)? Trenger egen, separat designrunde — se
+ FEATURE_BACKLOG.md for full analyse av hvorfor dette er vesentlig
+ vanskeligere enn del 1.
5. **Deltaker-tilgang til lag-chat/scorekort er nå BYGGET OG LIVE**
(2026-07-21, se status over) — ADR-031s tidligere notert begrensning
er dermed tettet.
diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md
index a4d1d09..db763d0 100644
--- a/FEATURE_BACKLOG.md
+++ b/FEATURE_BACKLOG.md
@@ -1315,7 +1315,7 @@ hvilke "første handling"-alternativer dashbordet bør vise i fremtiden.
---
-## Én person, flere e-postadresser — 📋 NOTERT 2026-07-20, IKKE designet/bygget
+## Én person, flere e-postadresser — DEL 1 (det enkle tilfellet) ✅ BYGGET OG LIVE 2026-07-21, DEL 2 (kontosammenslåing) fortsatt 📋 NOTERT
Reist av brukeren rett etter ADR-032 (verifisert e-postbytte). Et beslektet,
men DISTINKT behov: én person kan ha flere e-postadresser i omløp samtidig
@@ -1367,11 +1367,67 @@ naturlig ville hørt hjemme) — trolig fordi selve GEVINSTEN (nye turneringer
dukker opp) er noe som vises på dashbordet, så handlingen bør ligge der
resultatet vises.
-**Ingenting designet i detalj eller bygget ennå** — kun fanget grundig her
-slik at det ikke går i glemmeboken. Bør trolig deles i to separate runder:
-(1) legg til en frisk, ukrevd sekundær-e-post (rimelig godt avgrenset,
-gjenbruker ADR-032 sitt mønster direkte), (2) ekte konto-sammenslåing for
-det vanskelige tilfellet over (egen, større designrunde).
+**Ble delt i to separate runder, som foreslått:** (1) legg til en frisk,
+ukrevd sekundær-e-post — ✅ BYGGET, se under. (2) Ekte konto-sammenslåing
+for det vanskelige tilfellet — fortsatt IKKE designet, egen fremtidig
+runde.
+
+### Del 1 (det enkle tilfellet) — ✅ BYGGET OG LIVE 2026-07-21
+
+Migrasjon `017_secondary_email.sql`: to nye tabeller
+(`secondary_email_token` — midlertidig, samme token-hash-og-utløp-mønster
+som `email_change_token`; `user_secondary_email` — den faktiske,
+verifiserte adressen, globalt UNIQUE). Nye endepunkter i
+`app/routers/auth.py`: `POST /auth/secondary-email` (send
+bekreftelseslenke), `POST /auth/secondary-email/confirm` (ingen sesjon
+påkrevd, samme mønster som selve magic-link-verifiseringen),
+`DELETE /auth/secondary-email/{id}`.
+
+**Kjernestykket, ikke bare CRUD:** `verify_magic_link` og
+`login_with_password` sjekker nå `user_secondary_email` FØR de gjør sitt
+vanlige `app_user.email`-oppslag — finner de en match, løses innloggingen
+til DEN EKSISTERENDE eierens konto i stedet for å (som før) stille
+opprette en helt ny, separat konto. Dette er selve mekanismen som gjør
+adressen nyttig, ikke bare en liste over "andre adresser".
+
+**Plassering, bevisst avvik fra brukerens opprinnelige "fra dashbordet"-
+instruks:** lagt i `/account` (samme sted som ADR-032 sin e-postbytte),
+IKKE dashbordet — begrunnet med at dette kun er del 1 (det enkle
+tilfellet); når/hvis del 2 (kontosammenslåing, "data dukker opp") bygges,
+er dashbordet trolig riktigere siden GEVINSTEN vises der. Flagget
+eksplisitt til bruker, ikke stille besluttet.
+
+**Scratch-verifisert, 20 sjekker:** adresse legges IKKE til før bekreftet;
+token ikke gjenbrukbart; adresse som allerede er en ANNEN kontos
+hovedadresse ELLER sekundæradresse avvist tydelig (409 DUPLICATE) i begge
+retninger; innlogging via sekundæradressen (BÅDE magic-link OG passord)
+løses korrekt til samme, eksisterende konto (bekreftet: samme `id`,
+`email` i responsen forblir hovedadressen); en fremmed kan ikke slette
+andres sekundæradresse; og — den kritiske sjekken — en ny innlogging på
+adressen ETTER at den er fjernet oppretter en genuint NY, separat konto
+(beviser fjerningen er reell, ikke kosmetisk). `test_isolation.sql`
+fortsatt 12/12 (additiv migrasjon). Ekte typesjekket produksjonsbuild av
+frontend kjørt og bekreftet.
+
+**Rullet ut live 2026-07-21**, bruker bekreftet eksplisitt: migrasjon 017
+kjørt mot ekte `teecup_db` (bekreftet begge nye tabeller finnes,
+`test_isolation.sql` fortsatt 12/12), deretter `docker compose up -d
+--build teecup_api teecup_frontend`. Begge containere boot-et rent,
+`/health`/`/dashboard`/`/account`/`/verify-email` → 200, `teeoff.no`
+upåvirket.
+
+### Del 2 (ekte kontosammenslåing) — fortsatt 📋 NOTERT, IKKE designet
+
+Uendret fra den opprinnelige analysen: hva skjer hvis adressen som legges
+til ALLEREDE er primær- eller sekundæradressen til en ANNEN, eksisterende
+konto (spilleren har altså to helt separate kontoer med egen historikk —
+ulike org-medlemskap, ulike spillerkoblinger, kanskje ulikt passord/2FA)?
+Dagens del 1-løsning avviser dette tydelig (409 DUPLICATE) i stedet for å
+gjette — en ekte sammenslåing (slå sammen org-medlemskap uten å bryte
+"én rolle per bruker per org", deduplisere spillerkoblinger, avgjøre
+hvilken konto som "vinner" for motstridende felt) er en betydelig større
+og mer risikofylt operasjon, fortsatt bevisst utsatt til en egen,
+dedikert designrunde.
---
diff --git a/app/routers/auth.py b/app/routers/auth.py
index 4325934..2edd491 100644
--- a/app/routers/auth.py
+++ b/app/routers/auth.py
@@ -780,13 +780,48 @@ async def update_profile(
values.append(user.user_id)
async with plain_connection() as conn:
+ # HCP-historikk (FEATURE_BACKLOG.md, ADR-031 sitt "naturlig neste
+ # steg"-punkt): les gjeldende verdi FØR den overskrives, slik at vi
+ # kan avgjøre om dette faktisk er en ENDRING (ikke bare et PATCH-
+ # kall som gjentar samme verdi) FØR vi logger en ny historikk-rad.
+ old_hcp = None
+ if "handicap_index" in updates:
+ old_hcp = await conn.fetchval("SELECT handicap_index FROM app_user WHERE id = $1", user.user_id)
+
await conn.execute(
f"UPDATE app_user SET {', '.join(set_clauses)} WHERE id = ${len(values)}",
*values,
)
+
+ if "handicap_index" in updates:
+ new_hcp = updates["handicap_index"]
+ # Kun faktiske tallverdier logges (ikke nullstilling) -- en
+ # "HCP fjernet"-hendelse gir ingen mening i en verdi-over-tid-
+ # historikk.
+ changed = new_hcp is not None and (old_hcp is None or float(old_hcp) != float(new_hcp))
+ if changed:
+ await conn.execute(
+ "INSERT INTO handicap_history (user_id, handicap_index) VALUES ($1, $2)",
+ user.user_id,
+ new_hcp,
+ )
return await me(user)
+@router.get("/profile/handicap-history")
+async def get_handicap_history(user: CurrentUser = Depends(get_current_user)) -> list[dict]:
+ async with plain_connection() as conn:
+ rows = await conn.fetch(
+ """
+ SELECT handicap_index::float AS handicap_index, recorded_at
+ FROM handicap_history WHERE user_id = $1
+ ORDER BY recorded_at
+ """,
+ user.user_id,
+ )
+ return [{"handicap_index": r["handicap_index"], "recorded_at": r["recorded_at"].isoformat()} for r in rows]
+
+
@router.post("/profile/avatar", response_model=Me)
async def upload_avatar(
file: UploadFile,
diff --git a/frontend/components/account-settings.tsx b/frontend/components/account-settings.tsx
index 75911d2..0f8020f 100644
--- a/frontend/components/account-settings.tsx
+++ b/frontend/components/account-settings.tsx
@@ -403,10 +403,80 @@ function ProfileSection({ me, onChanged }: { me: Me; onChanged: () => void }) {
{saving ? "Lagrer …" : "Lagre profil"}
+
+
Laster …
+ ) : entries.length === 0 ? ( ++ Ingen historikk ennå — registreres automatisk neste gang du endrer HCP. +
+ ) : ( +