diff --git a/README.md b/README.md
index cf12b4c..65599cd 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ GraphQL-backend i Node og PostgreSQL.
## Prøv appen
-Appen kjører på gruppens VM:
+Appen kjører på gruppens VM:
VM-en er bare tilgjengelig fra NTNU-nettet. Utenfor campus må du koble til
[NTNU VPN](https://i.ntnu.no/vpn) først. Hvis VM-en ikke svarer, kan appen
@@ -17,25 +17,44 @@ kjøres lokalt med egen database og backend, se
## Om prototypen
-Første innlevering var en klikkbar **frontend-prototype** som viste idéen og
-hvordan løsningen er tenkt brukt.
-
-**Innlevering 2** kobler prototypen til en ekte database og backend:
-
-- **Backend:** PostgreSQL med migrasjoner og seed, og et GraphQL-API i Node
- og TypeScript i `backend/`. Backenden kjører som en systemd-tjeneste på VM-en,
- og Apache sender `/project2/api/` videre til den.
-- **Katalogen** henter annonsene fra backenden, 24 om gangen med de nyeste
- først. Databasen på VM-en har 10 010 annonser.
-- **Detaljvisningen** henter én annonse fra backenden, også når lenken åpnes
- direkte.
-- **Utleieskjemaet** lagrer nye annonser i databasen. Serveren validerer dem
- på nytt, og den nye annonsen vises øverst i katalogen.
-
-Leieforespørselen er fortsatt demo. Søk, filtre og sortering i katalogen
-gjøres på serveren, i hele databasen, se
-[Søk, filtre og sortering](#søk-filtre-og-sortering). Brukere kan også gi
-annonser en vurdering fra 1 til 5, se [Vurderinger](#vurderinger). Se også
+**Innlevering 3** er en betaversjon der hele kjernen i appen virker: søk,
+filtre, sortering, blaing, detaljvisning og lagring av brukerdata, mot en
+database med 10 010 annonser på VM-en.
+
+| Innlevering | Hva den inneholdt |
+| ----------- | --------------------------------------------------------------------------------------------------------------- |
+| 1 | Klikkbar frontend-prototype med mockdata som viste idéen og visningene |
+| 2 | PostgreSQL og GraphQL-backend på VM-en. Katalogen, detaljvisningen og utleieskjemaet koblet til backenden |
+| 3 | Søk, filtre og sortering i hele databasen, vurderinger, tilgjengelighet, bærekraft og ende-til-ende-tester (ny) |
+
+**Nytt i innlevering 3:**
+
+- **Søk, filtre og sortering på serveren.** Søk i tittel og beskrivelse, med
+ filtre for kategori, område, tilstand, døgnpris og gratis utlån, og
+ sortering på nyeste eller pris. Alt gjøres i databasen før resultatet deles
+ opp i sider, se [Søk, filtre og sortering](#søk-filtre-og-sortering).
+- **Aktive filtre og tomt resultat.** Valgte filtre vises som brikker som kan
+ fjernes én og én. Et søk uten treff sier hva som ble søkt på og har en knapp
+ for å vise alle annonser.
+- **Veien tilbake.** Søk, filtre, sortering og side ligger i URL-en. Fra en
+ annonse kommer brukeren tilbake til samme søk og side, uten ny forespørsel
+ til serveren.
+- **Vurderinger.** Brukere kan gi en annonse 1–5 stjerner og en valgfri
+ kommentar.
+ Vurderingene lagres i databasen, og nytt snitt vises med en gang, se
+ [Vurderinger](#vurderinger).
+- **Tilgjengelighet.** Gått gjennom mot WCAG 2.1 AA med axe-core, tastatur,
+ 320 px bredde og tekstavstand. Funnene er rettet, og axe kjører i testene,
+ se [docs/tilgjengelighet.md](docs/tilgjengelighet.md).
+- **Bærekraft.** Målt datamengde før og etter tiltak. Komprimering og
+ cache-headere i Apache gjør et nytt besøk 2,2 kB i stedet for 5,7 kB, se
+ [docs/baerekraft.md](docs/baerekraft.md).
+- **Ende-til-ende-tester** med Playwright av søk, filtre, sortering, blaing,
+ veien tilbake, ny annonse og vurdering, se
+ [docs/testing.md](docs/testing.md#ende-til-ende-tester-playwright).
+
+Valgene bak løsningen står under
+[Valg og begrunnelser](#valg-og-begrunnelser). Se også
[Frontend mot GraphQL](#frontend-mot-graphql) og
[Backend og database](#backend-og-database).
@@ -51,7 +70,7 @@ låne verktøy i nærheten i stedet for å kjøpe nytt.
| Visning | Slik kommer du dit | Hva du kan gjøre |
| -------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
-| Katalog | Startsiden, «Katalog» i menyen eller navnet øverst | Søke, filtrere på kategori og gratis utlån, sortere og bla, 24 om gangen |
+| Katalog | Startsiden, «Katalog» i menyen eller navnet øverst | Søke, filtrere på kategori, område, tilstand, pris og gratis utlån, sortere og bla, 24 om gangen |
| Detaljvisning | Klikk på et annonsekort i katalogen | Se bilde, beskrivelse, tilstand, tilbehør, pris og vurderinger, gi en vurdering, velge datoer og se samlet pris |
| Lei ut verktøy | «Lei ut verktøy» i menyen | Fylle ut skjemaet for en ny annonse med tittel, kategori, beskrivelse, område og pris |
@@ -108,31 +127,17 @@ mens selve annonsene genereres. Se [docs/datasett.md](docs/datasett.md).
### Planlagt videre
-- **Søk i hele databasen:** Søk, filtre og sortering skjer på serveren, før
- resultatet deles opp i sider. Katalogen får blaing med «Forrige» og
- «Neste», og endrede valg starter på første side. Søket venter til brukeren
- har sluttet å skrive (debouncing), og svar på utdaterte søk forkastes.
-- **State:** Apollo Client henter og bufrer allerede data fra backenden. Søk,
- filtre og sortering skal fortsatt ligge i URL-en, slik at de kan deles som
- lenke.
+- **Til innlevering 4:** Test med skjermleser (VoiceOver eller NVDA), flere
+ tester av utypisk bruk og snapshot-tester, se
+ [docs/testing.md](docs/testing.md#gjenstående-kontroller) og
+ [docs/tilgjengelighet.md](docs/tilgjengelighet.md#gjenstår).
- **Innlogging og egne annonser:** Brukere kan opprette, redigere og
deaktivere egne annonser. Serveren sjekker at bare eieren kan endre dem.
- **Booking:** Eieren godkjenner eller avslår leieforespørsler, og begge
parter ser sine leieforhold. Godkjente bookinger kan ikke overlappe for
samme verktøy. Utlevering og retur registreres, og leietakeren kan gi en
- vurdering etterpå. Gratis utlån bruker samme flyt.
-- **Tilgjengelighet:** Appen er gått gjennom mot WCAG 2.1 AA med axe-core,
- tastatur, 320 px bredde og `prefers-reduced-motion`, og funnene er rettet,
- se [docs/tilgjengelighet.md](docs/tilgjengelighet.md). Test med skjermleser
- gjenstår.
-- **Bærekraft:** Små bilder som lastes når de trengs, systemfont, mørkt tema,
- komprimering og cache-headere i Apache, og spørringer som bare henter feltene
- som vises. Målingene før og etter står i
- [docs/baerekraft.md](docs/baerekraft.md).
-- **Testing:** Komponenttester med Vitest, API-tester mot en testdatabase og
- ende-til-ende-tester med Playwright finnes allerede, se
- [docs/testing.md](docs/testing.md). Senere kommer en ende-til-ende-test av en
- hel leieflyt, når booking finnes.
+ vurdering etterpå. Gratis utlån bruker samme flyt. Da kommer også en
+ ende-til-ende-test av hele leieflyten.
Betaling, depositum, frakt og chat er ikke en del av planen. Hvis kjernen blir
ferdig og testet, vurderer vi prosjektlister, for eksempel «Bygg en
@@ -140,27 +145,106 @@ bokhylle», med verktøyene som trengs.
### Dette ønsker vi tilbakemelding på
-**Oppsett og samspill mellom klient og backend:**
-
-- Var det lett å finne ut hvordan appen kjøres, både på VM-en og lokalt? Hvor
- stoppet du opp?
-- Er det forståelig hvordan Apache, backenden og databasen henger sammen på
- VM-en, ut fra [docs/vm-backend.md](docs/vm-backend.md) og
- [deploy/apache/README.md](deploy/apache/README.md)?
-- Er GraphQL-skjemaet i `backend/src/graphql/schema.ts` lett å forstå? Ville
- du delt opp spørringene eller feltene annerledes?
-- Føles katalogen rask nok når du blar, og er det tydelig hva som skjer mens
- data lastes eller hvis backenden ikke svarer?
-- Dukket annonsen du la ut via «Lei ut verktøy», opp i katalogen slik du
- forventet?
-
-**Idé og brukeropplevelse:**
-
-- Er idéen og forskjellen på leie og gratis utlån lett å forstå?
-- Hvilke filtre og sorteringer ville du brukt mest når søket kommer tilbake,
- for eksempel område eller ledige datoer?
-- Er det tydelig hvordan en leieforespørsel fungerer?
-- Er det noe i datamodellen over som virker unødvendig, eller som mangler?
+**Søk og filtre:**
+
+- Finner du raskt det du leter etter? Prøv for eksempel gratis drill på Moholt,
+ eller en stige under 100 kr per døgn.
+- Er det tydelig hvilke filtre som er valgt, og hvordan du fjerner dem? Kom du
+ noen gang til et tomt resultat uten å skjønne hvorfor?
+- Kommer du tilbake til samme søk og side når du åpner en annonse og går
+ tilbake, både med lenken og med nettleserens tilbakeknapp?
+- Mangler det et filter eller en sortering du ville brukt, for eksempel
+ nærmeste område eller best vurdert?
+
+**Tilgjengelighet:**
+
+- Kan du bruke hele appen med bare tastatur, og ser du alltid hvor fokus er?
+- Har du skjermleser, og fungerer søket og antall treff med den? Vi har ikke
+ testet med skjermleser selv ennå.
+- Ser du noe som bryter med WCAG som vi ikke har fanget i
+ [docs/tilgjengelighet.md](docs/tilgjengelighet.md)?
+
+**Bærekraft:**
+
+- Er målingene i [docs/baerekraft.md](docs/baerekraft.md) overbevisende, og
+ måler vi det som betyr noe?
+- Ser du tiltak vi har oversett, for eksempel i bundle-størrelsen eller i
+ hvordan katalogen henter data?
+
+**Kodekvalitet:**
+
+- Er koden lett å sette seg inn i? Prøv for eksempel å følge et søk fra
+ `CatalogSearch` til SQL-en i `backend/src/db/listingsQuery.ts`.
+- Er det deler som er vanskelige å forstå, eller som burde vært delt opp?
+- Er testene i [docs/testing.md](docs/testing.md) dekkende, og hva ville du
+ testet i tillegg?
+
+**Oppsett:**
+
+- Var det lett å kjøre appen, både på VM-en og lokalt? Hvor stoppet du opp?
+
+## Valg og begrunnelser
+
+### Søk, filtre og skalering
+
+Søk, filtre og sortering gjøres i PostgreSQL før resultatet deles opp i sider
+på 24 annonser. Klienten henter derfor bare én side om gangen, og filtrene
+gjelder hele databasen, ikke bare det som er lastet inn. `totalCount` gir
+antall treff i hele databasen, så katalogen kan vise riktig antall treff og
+sider.
+
+Løsningen er laget for å tåle mye mer data enn vi har:
+
+- **Indekser** for sortering, søk i tekst (trigram, `pg_trgm`), kategori,
+ område og pris. Med 50 000 annonser tar søk etter vanlige og sjeldne ord
+ under 2 ms, mot rundt 50 ms uten indeks. Svært vanlige ord, som «til», er
+ fortsatt tregere. Målingene og begrensningene står i
+ [Indekser for søk og filtre](docs/datasett.md#indekser-for-søk-og-filtre).
+- **Blaing med `limit` og `offset`.** De første sidene tar like kort tid
+ uansett datamengde. Sider langt bak blir litt tregere, men det merkes lite
+ med 50 000 annonser. Målingene og alternativet med cursor-paginering står i
+ [Paginering og indeks](docs/datasett.md#paginering-og-indeks).
+- **Færre forespørsler.** Søket sendes først når brukeren har sluttet å
+ skrive (debouncing), og Apollo-cachen gjør at en side som er hentet før,
+ vises uten ny forespørsel.
+- **Ingen N+1.** Snitt og antall vurderinger regnes ut i samme spørring som
+ annonsene, ikke med én ekstra spørring per kort.
+
+### State management
+
+Vi bruker ikke et eget state-bibliotek som Redux, fordi tilstanden i appen
+deler seg naturlig i tre:
+
+| Tilstand | Hvor den ligger | Hvorfor |
+| ------------------------------------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| Data fra serveren | Apollo Client sin cache | Normalisert cache: en annonse som er oppdatert ett sted, for eksempel med ny vurdering, oppdateres overalt. Laste- og feiltilstand følger med |
+| Søk, filtre, sortering og side | URL-en | Overlever sideoppdatering, kan deles som lenke og virker med nettleserens tilbakeknapp |
+| Siste søk til «Katalog» i menyen | `sessionStorage` | Søket huskes i fanen, men dukker ikke opp igjen neste dag, se [docs/sok-og-filtre.md](docs/sok-og-filtre.md#katalog-i-menyen-husker-søket) |
+| Skjemafelt, åpne paneler og lignende | Vanlig React-state | Gjelder bare én komponent |
+
+Et globalt lager ville kopiert data som allerede finnes i Apollo-cachen eller
+URL-en, og måtte holdes i takt med dem.
+
+### Komponenter og biblioteker
+
+Brukergrensesnittet er laget med egne komponenter og CSS-moduler, uten et
+komponentbibliotek. Vi har full kontroll over semantikken og
+tilgjengeligheten, og bunten blir mindre, se
+[JS-bunten](docs/baerekraft.md#js-bunten). Bibliotekene vi bruker:
+
+| Bibliotek | Brukes til | Hvorfor |
+| --------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
+| React 19 og TypeScript | Brukergrensesnittet | Krav i emnet |
+| React Router | Ruter og valgene i URL-en | `useSearchParams` gjør URL-en til tilstand for søk og filtre |
+| Apollo Client (med `graphql` og `rxjs`) | Henting og cache av data | Laget for GraphQL, med normalisert cache, `MockedProvider` til testene og oppdatering av cachen etter mutasjoner |
+| Apollo Server 5 | GraphQL-API-et | Utbredt og godt dokumentert, og lite kode for å sette opp skjema og resolvere |
+| `pg` (node-postgres) | Spørringer mot PostgreSQL | Vi skriver SQL selv med parametere og har kontroll over indekser og spørreplaner, uten en ORM imellom |
+| `node-pg-migrate` | Endringer i databasen | Versjonerte migrasjoner som kan kjøres likt lokalt, i testdatabasen og på VM-en |
+| `tsx` | Kjøre backenden | Kjører TypeScript direkte, uten et eget byggesteg, både lokalt og i systemd på VM-en |
+| Vitest, React Testing Library og jsdom | Komponent- og enhetstester | Bruker samme oppsett som Vite. Testing Library finner elementer slik en bruker gjør, med rolle og navn |
+| axe-core | Automatisk sjekk av tilgjengelighet | Samme motor som Lighthouse, og kjører både i testene og i nettleseren |
+| Playwright | Ende-til-ende-tester | Styrer en ekte nettleser mot backenden og testdatabasen |
+| ESLint og Prettier | Kodekvalitet og formatering | Krav i emnet. `eslint-config-prettier` hindrer at de to krangler |
## Krav