# Frontend v3: Parity Plan

Last updated: 7 October 2026 · Maintained by: Mitko (with Claude Code) · Readers: development team and product owner

**This plan replaces the plan of the Vue 3 rewrite.** The rewrite project (`kulibri-rewrite`, built by the Bolt team) is finished and merged: its final state was imported into the folder `frontend-v3` of this repository on 7 October 2026 and the standalone repository is frozen. There is only one new frontend now, `frontend-v3`, called **the new UI** in this document. The history of the merge, the old plan and the rewrite's own documents are in [`docs/archive/`](docs/archive/README.md).

---

## 1 · Summary for the product owner

**The goal.** The new UI does everything the classic app does for users today, so that the classic app can be switched off. We get there in three phases, and users help us on the way.

**Where we are.** The new UI is built into the app and runs next to the classic one at the same web addresses. A user who chooses it ("Try the new design" in the classic menu; "Classic view" in the new one) gets the new dashboard, and, for the poll types we switch on, the new poll page. **Everything the new UI cannot do yet opens in the classic view automatically**, so nobody reaches a dead end, and every link already sent by email keeps working. Setting one value on the server (`KULIBRI_UI_V3_ENABLED=false`) sends everybody back to classic at once.

**What the new UI can do today** (built and covered by automated tests; most of it is not yet compared screen by screen with the classic app in a browser):
- the dashboard (own and invited polls, search, archive, guest-list counts as in classic);
- date polls: answering, comments, tasks, participants, invitations, links, the poll editor, closing, copying, deleting, Excel export, appearance and design;
- text polls and guest lists (built, to be compared);
- membership, invoices and company invoices, profile, password change, account deletion for every user, unsubscribe;
- new in the new UI: live updating, large polls load in parts, an account status page and status chip.

**What is still missing**, in plain words:
- **Sign-in, registration, password reset and email confirmation** stay in the classic app. They must be rebuilt before the classic app can be switched off.
- **The course planner (workshops)** is only partly there (admin cancelling a booking, the waiting list decided by the server, the attendee list per course).
- **English:** most new screens are German only.
- A handful of editor settings and rarely used functions (see the checklist in section 5).

**In numbers.** The checklist in section 5 has 87 rows. Status by code inspection on 7 Oct: 49 built (not yet compared with classic in a browser), 14 partial, 11 missing, 6 served by classic, 5 waiting for a decision. **40 rows are needed before the first users are invited**: 33 of them are built and need the audit, 6 need work (4 partial, 2 missing), and 1 needs a decision. The other 47 can be closed during the parallel run or later.

**The plan in three phases**

| Phase | What | When it ends |
|---|---|---|
| **1 · Audit and readiness** | Compare what is built with the classic app in a browser, build what the first poll types need, set up the feedback channel, switch on the first poll type. | The first poll type is offered to users, and the points in section 6.1 are done. |
| **2 · Parallel run, up to 3 months** | Classic and new run side by side. Users who want to try the new UI choose it, and we collect their feedback. We keep closing the remaining gaps and switch on more poll types and pages. Classic stays the default. | When the retirement criteria (section 6.3) are met, **at the latest three months after phase 2 starts**. |
| **3 · Retire classic** | Rebuild sign-in and the other classic-only pages, make the new UI the default, keep classic reachable for a short grace period, then remove it. | Classic is removed. |

**What we need from you** (details and our proposal in section 7):
1. Which poll type goes first, and in what order the others follow (we propose date polls, then text polls, then guest lists, then workshops).
2. The default UI during the parallel run (we propose: classic stays the default).
3. Hidden polls: keep them in classic until the server stops sending other participants' answers (we propose: yes), or let the new UI hide them itself.
4. The counting rule for guest lists (PO-26), and the texts for account deletion and unsubscribe (PO-08, PO-11), before real users see them.
5. Which "Premium" markers stay, which are removed (13 markers, some of them without a function).
6. How we measure "ready to retire": proposal in section 6.3. We need your numbers.

---

## 2 · Goal, scope and product rules

**Goal.** Frontend v3 reaches **parity** with the classic frontend: every function the classic app offers to users today is available in the new UI with the same behaviour, or with a difference that is deliberate and written down. Old links keep working. Then the classic app is retired. After that the new UI is the only frontend and grows with new features.

**What parity means.** One row of the checklist (section 5) is *done* when the new UI offers the function, the behaviour matches the classic one (or the difference is documented in the module header and in the checklist), it is covered by automated tests, and someone has compared it with the classic app on the same poll in a browser (`?classic=1` gives the classic view with the same session).

**Out of scope for parity (new work, separate track, section 6.4).** The grid concept with server-side paging and inline editing (`GRID-KONZEPT.md`), shift planning with two-phase allocation, the address list, one-to-one appointments and expense accounting. They do not block retiring classic.

**Product rules that apply to every row** (from the product strategy; they decide conflicts):
- Functions that bring in new participants (invite, copy, self sign-up) are **never priced**.
- **No payment module:** no money flows between participants through the platform. kulibri's own subscription (36 euro a year) is a different thing.
- **Participants need no account.**
- When a design template (mockup or screenshot) is given, it is implemented 1:1; existing functionality stays; a conflict between template and function is asked about before anything is changed.
- Differences from Doodle that stay: no ads for participants, "if needed" answers always free, unlimited active polls free, servers in Germany, GDPR.

---

## 3 · Decisions (carried over, with dates)

| Date | Decision | By |
|---|---|---|
| 6 Oct | **Same link (SL):** one link per poll; the visitor decides which UI opens it (`?ui=`, a cookie, `users.ui_version`; default classic). The invitee decides, not the sender. The alternative, a different link pattern for the new UI, is documented in `docs/archive/migration/REWRITE-INTEGRATION.md`, section 14. | PO |
| 6 Oct | **Session login in both UIs.** Both run on one host and share the Laravel session. Login stays in classic until the new UI has its own sign-in pages (phase 3). | PO |
| 6 Oct | **One design system:** the tokens of the rewrite (`--k-*`). The earlier `frontend-v3` palette is gone (A-24). | PO |
| 6 Oct | **The dashboard counts as classic does** (A-23). The guest-list card no longer counts guests twice. | PO |
| 7 Oct | **One cut, never two live copies.** All rewrite code is in `frontend-v3`; the standalone repository is frozen; bugs are fixed in `frontend-v3`. Bolt does not work on the code any more (last day 7 Oct, questions possible for two weeks). | PO |
| 6 Oct | At migration start the rewrite was **assumed correct**; issues found since go through the usual bug process (A-22). | PO |
| 7 Oct | **Parallel run of up to three months** with user feedback, then classic is retired (this plan). | PO |
| 7 Oct | **Archive per user** (G18, PO priority 1): archiving a poll no longer hides it for everybody. Backend built on 7 Oct, see section 6.2. | PO |
| 7 Oct | Polls with **tasks** are served by the new UI; polls with **custom participant fields** stay in classic until the fields are verified (switch `KULIBRI_UI_V3_FEATURE_GUARD`). | team |
| 8 Oct | **Premium** (PO): hidden mode, the Excel export, kulibri-sent invitations above a free limit and messages to participants are Premium; hidden mode stays on polls that already have it; enforced on the server as well as in the UI. The free invitation limit is 50 (proposal, to confirm). | PO |

The product owner's earlier questions and answers (PO-01 to PO-26, UI questions, A-18 to A-24) are in `docs/archive/rewrite-repo/docs/po-fragen.md` and `ui-po-fragen.md` (German).

---

## 4 · How classic and new run side by side

**Who decides.** For each request the server (`App\Kulibri\Ui\UiVersionResolver`) picks the page shell. Classic is always the fallback. The new UI is served only if **all** of these hold:

1. `KULIBRI_UI_V3_ENABLED=true` (the master switch);
2. the path is claimed in `config/kulibri_ui.php` `v3_paths` (today: `dashboard`, `pr/new`, `new` and `preview/*`), **or** it is a poll link (`/pr/...`) of a poll type listed in `KULIBRI_UI_V3_POLL_TYPES` (comma separated: 1 date poll, 2 guest list, 3 text poll, 4 workshop; empty means none);
3. the visitor chose the new UI (`?ui=v3` > cookie `kulibri_ui` > `users.ui_version`; nobody chosen means classic);
4. for poll links: the poll is not hidden (`KULIBRI_UI_V3_HIDDEN_POLLS=false`) and has no custom participant fields (unless `KULIBRI_UI_V3_FEATURE_GUARD=false`; `.env.example` has it false for development, the code default is true).

The response headers `X-Kulibri-UI` and `X-Kulibri-UI-Reason` say what was served and why (for example `poll-type-legacy`, `poll-hidden`, `poll-features-legacy`, `query`, `cookie`). **This is the first thing to look at when a user reports "I got the wrong UI".**

**Switching controls.** "Try the new design" in the classic user menu, "Classic view" in the new one, and `?classic=1` for a one-off classic view of one page (no cookie, no account change, and it survives redirects). `?ui=legacy` and `?ui=v3` are the sticky switches.

**Pages the new UI does not have.** Any path the new UI does not route itself is handed over with a full page load, so classic opens it (`LegacyPageView`, `router/handover.ts`). To take a page over, add both its route in `frontend-v3/src/router/index.ts` and its entry in `v3_paths`, and only when the page is complete.

**Links.** All mail links are `/pr/...` and the server decides per visitor. Link formats and how the second segment is read are in [`docs/route-map.md`](docs/route-map.md). Public keys, legacy admin keys, invitation ids and the three-part link all work in the new UI.

**Safe way back.** `KULIBRI_UI_V3_ENABLED=false` serves classic to everybody immediately. Per poll type, remove the type from `KULIBRI_UI_V3_POLL_TYPES`. Both are configuration, no deploy.

---

## 5 · Parity checklist

How to read it. **Ref** is the row number of the old gap board (`docs/archive/rewrite-repo/PARITY-GAPS.md`, status 6 Oct) so that nothing gets lost; rows added since have no number. **Status** was checked against the code of `frontend-v3` on 7 Oct 2026 (files, tests, the route table), not copied from the old board.

| Status | Meaning |
|---|---|
| **Done** | built, tested, and compared with classic in a browser |
| **Built** | built and covered by tests, **not yet compared with classic in a browser** (the phase 1 audit turns these into Done) |
| **Partial** | part of it exists; the note says what is missing |
| **Missing** | not built |
| **Classic** | classic serves it; the new UI has no page yet (handover) |
| **Decision** | a product decision is needed first |
| **No** | not a classic function, or dropped on purpose |

**Needed for:** **Run** = before users are invited to the new UI for this area (phase 1); **Retire** = before classic is switched off (can be closed during phase 2); **Later** = rare or optional, scheduled after retirement unless a user asks.

### A · Account and access

| Ref | Function | Status | Needed for | Note |
|---|---|---|---|---|
| — | Sign-in page | Classic | Retire | The rewrite had its own (token login); dropped in the cut because both UIs share the session. To rebuild on the session login in phase 3. The route `login` hands over. |
| 1.1 | Registration | Classic | Retire | Same: built in the rewrite, dropped in the cut. Source and texts in the archive. |
| 1.2 | Email confirmation | Classic | Retire | "Resend the confirmation mail" needs a backend route (G10). |
| 1.3 | Forgot and reset password | Classic | Retire | Mail links point to `KULIBRI_FRONTEND_URL`. |
| 1.4 | Google sign-in | Classic | Retire | `POST /api/auth/google` exists for token clients; with the session the classic flow stays until phase 3. |
| 1.5 | Profile (name, email, language) and password change | Built | Run | Side panels in the user menu (`ProfilePanel`, `PasswordPanel`); invoice data in the profile (see 2.2). |
| 1.6 | Delete account | Built | Run | Available to **every** user (classic: only admins). Texts and reasons need PO confirmation (PO-08). |
| 1.7 | Unsubscribe from mails | Built | Retire | Page `/unsubscribe` exists but is not claimed yet; links in sent mails. Text and fate of the setting: PO-11, finding G15 (the flag is not read anywhere). |
| 1.8 | "Poll deleted" page | Partial | Run | Done for deleted polls. The second trigger (deleted invitation, see H1) is open. |
| 1.9 | Terms and privacy | Built | Retire | `/agb` and `/dataprivacy` redirect to the marketing site (PO-10). |
| 1.10 | Language (de, en) | Partial | Run (core poll screens), Retire (all) | German and English message files exist, but many screens still contain German text in the template (the poll page and the editor most of all). Rule 7 of `docs/frontend-v3-rules.md`. |
| 1.11 | "Promotion shown" hint | Missing | Later | Check what classic does with it (`POST /user/promotion-shown`). |
| — | Session expiry (419) | Built | Run | After expiry a write returned 419 although the remember cookie would sign the user in again. Measure 2 is built in both UIs: on a 419 the client fetches `/sanctum/csrf-cookie` and repeats the write once (classic: `frontend/src/shared/csrfRetry.js`, on `master` and `vue3-redesign`; new UI: `send()` in `frontend-v3/src/api/http.ts`, also for public-page requests). Open: (1) measure 1, read `SESSION_LIFETIME` on production (`Max-Age` of `GET /sanctum/csrf-cookie`, divided by 60; test server 120 minutes, code default 2; raise it only if it is a few minutes, never to days); (2) the browser check in both UIs: delete only the session cookie, keep `remember_web_…`, then write (add a comment). Background: `docs/archive/migration/REWRITE-INTEGRATION.md`, section 5. |

### B · Membership and invoices

| Ref | Function | Status | Needed for | Note |
|---|---|---|---|---|
| 2.1 | Membership and subscription (Mollie) | Built | Retire | Page `/membership` exists; not claimed (Mollie returns there). Mollie test key checks for abort, failed payment, cancel and the invoice after upgrade are open. |
| 2.2 | Invoice list, printable invoice, company invoices | Built | Retire | Billing profile (`GET/PUT /api/billing-profile`) and numbered invoices exist. Open: tax advisor check (mandatory details, number range, tax for foreign companies), invoice by mail and as a stored PDF. |
| 2.3 | Cancel and renew | Built | Retire | Trial deliberately not carried (the route was removed for security). |
| 2.4 | Feature locks by plan, Premium markers | Decision | Retire | 13 "PREMIUM" markers; at least "send reminder" and "co-admins" panel were dummies. Decide which stay. |
| — | Account status page, status chip in the dashboard header | Built | — | New, not in classic. |

### C · Poll types and answering

| Ref | Function | Status | Needed for | Note |
|---|---|---|---|---|
| 3.1 | Date poll (type 1) | Built | Run | First type to switch on. Reading the poll, the link formats and the dashboard were checked in a browser; the answering and editing flows still need the audit. |
| 3.3 | Text poll (type 3) | Built | Run | Second type. |
| 3.2 | Guest list (type 2) | Built | Run | After the PO decides the counting rule (PO-26, "without double counting"). |
| 3.4 | Workshop and course planner (type 4) | Partial | Retire | Cards, capacity bar, selection summary, waiting list exist. Missing: admin cancels a booking (10.4.1, `POST /participant/cancelBooking/{id}`), waiting list decided by the server instead of the browser (10.4.2), attendee list and statistics per course (10.4.4). Stays on classic until done. |
| 3.5 | Answer options yes, no, if needed, don't know | Built | Run | |
| 3.6 | "Unanswered counts as no" | Partial | Later | Displayed correctly; no switch in the editor (10.3.2). |
| 3.7 | Hidden poll, statistics for participants | Decision | Run | Built in the new UI. Hidden polls stay classic (resolver) until SEC-009 is fixed or the PO accepts the new UI hiding answers itself. Own row recognition by cookie for anonymous visitors is open (9.8). |
| 3.8 | Hide counts | Missing | Later | The value is read for display; no editor switch. |
| 3.9 | One choice per participant | Built | Run | Editor switch and rule exist. |
| 3.10 | Limit per option (slots) | Built | Run | `choice.limit`: per slot, all, per day. |
| 3.11 | Limit per event or per participant | Missing | Retire | Only the fields exist. |
| 3.12 | Guest limit per entry | Built | Run | |
| 3.13 | Extra answers per cell | Missing | Retire | Classic has `ConfigureAnswer` and `POST /{id}/remove-choice-answers`. First check how many polls use it. The two-phase allocation requirement builds on it. |
| 3.14 | Time zone | Partial | Later | Read and sent; no selector in the editor. |
| 3.15 | All option types of the editor (time ranges, days plus text) | Built | Run | Tick every type of the classic `Create/` components off in the audit. |
| 3.16 | Sort and filter in the grid | Partial | Later | Sorting (A to Z, added) and search exist; the classic `options.filters` are not compared. |
| 3.17 | Mobile layout | Built | Run | Needs a browser check of every view at phone width (comments, invite, edit participant, editor). |
| 3.18 | Edit answer: warning, partial answers | Built | Retire | Compare texts and cases with the classic locale. |
| 3.19 | Live updating | Built | — | New; classic has none. |
| — | Large polls (580 participants, 38 dates) | Built | Run | Loaded in parts; further server-side paging is in `GRID-KONZEPT.md`. |
| 9.4 | Limit exceeded errors | Built | Run | Checked on all save paths of date polls and of the course planner. |

### D · Participants, comments, tasks

| Ref | Function | Status | Needed for | Note |
|---|---|---|---|---|
| 4.1 | Add, rename, delete participants; change answers | Built | Run | Extracted into `useParticipantAdmin` and three dialogs (structural cut 5). |
| 4.2 | Custom participant fields | Built | Run | Verify every field type, mandatory and admin-only, then switch the resolver guard off. |
| 4.3 | Comments | Built | Run | Targeted comments need new backend columns. |
| 4.4 | Comment notification toggle per participant | Built | Retire | |
| 4.5, 4.6 | Tasks and their assignments ("who brings what") | Built | Run | Polls with tasks are served by the new UI since 7 Oct. |
| 4.7 | Sub-admins (co-admins) | Built | Retire | Backend open: remove the flags when the creator's subscription ends (F9). |
| 4.8 | Message to the admin (classic `SendMessageToAdmin`) | Missing | Retire | Hidden polls have a "messages to admin" tab in the new UI; the general function is missing. |
| 4.9 | Reminder to participants | Built | Retire | Backend finding G16: the route does not check that the sender owns the poll. |
| 4.10 | Admin cancels a booking | Missing | Retire | See 3.4. |
| 5.4 | Personal link per participant | Built | Run | |
| 9.1 | Invitation "visited" | Missing | Retire | Classic `POST /invitation/visit/{id}`; the meaning is open (`Design.md`). |
| 9.2 | "Address already takes part" | Partial | Retire | Replaced by a code step (spec 6 Oct): the dialog and rules exist (`EmailCodeDialog`, `utils/emailCode.ts`) but wait for backend F7 stage 2. Until then classic behaviour. |
| 9.3 | Email compared without case | Partial | Retire | Compared lower-cased in the browser; check what the backend stores. |
| 9.6 | Participation link by mail after sending | Built | Retire | |
| 9.8 | Own row in a hidden poll | Partial | Retire | See 3.7. |

### E · Invite and share

| Ref | Function | Status | Needed for | Note |
|---|---|---|---|---|
| 5.1 | Invite by email | Built | Run | Open: the `{link}` placeholder and the limit of 50. |
| 5.2 | Invite from contacts | Built | Run | Verify in the audit. |
| — | Invite: pick people from another project (Premium) | Built | Run | Classic `EditInvites.vue` ("Wähle aus Deinen Projekten aus"): pick one of your own projects, then its people, into the same recipient list. Built in the new UI on 8 Oct (`utils/pollContacts.ts`, `GET /api/ownpolls`), with the Premium lock as in classic. **Two points for the PO (question 10):** classic offers a participant only when the address is already in your own contacts, and whether the Premium lock stays. Deliberate difference: the number next to a project counts the people it can really contribute, so it matches the list that follows. |
| 5.3 | Link invitation, invitation overview, delete an invitation | Partial | Retire | Delete invitation (`DELETE /invitation/{id}`) is missing. |
| 5.5 | Admin link | Built | Run | The mail with the link for managing the poll goes out when the poll is first saved, and the new UI triggers it since 8 Oct: it writes `options.initialised` through `POST /poll/options`, the only route that sends that mail (before, the new UI wrote it with the details and no mail was ever sent, for logged-in owners either). `GET /send-admin-links` in the backend stays a leftover. Open: browser check that the mail arrives. |
| — | QR code, share page | Built | Run | `PollShareView`. |

### F · Poll editor and appearance

| Ref | Function | Status | Needed for | Note |
|---|---|---|---|---|
| 6.1 | Title, description, type | Built | Run | |
| 6.2 | Deadline, "no deadline", reminders | Built | Run | Verify the effect of the deadline on editing. |
| 6.3, 6.13 | Close and open a poll, choose final options | Built | Run | `ClosePollPanel`. The backend does not enforce closing or the deadline (as in classic). |
| 6.4 | Delete poll | Built | Run | The backend only sets `type = 0`. Open: the delete route has no login check (SEC-013). |
| 6.6 | Background image: defaults, upload, managing own images | Built | Run | Verify deleting and the limit. |
| 6.7 | Logo: upload, delete, link | Built | Retire | Premium lock (classic) not applied yet: PO decision. The header does not link the logo. |
| 6.8 | Designs and branding templates | Partial | Retire | Manage, preview, save as; check which design settings classic shows. |
| 6.9 | Dark mode of a poll | Built | Run | |
| 6.10 | Admin notifications (new answer, new participant) | Built | Run | |
| 6.11 | Copy a poll | Built | Run | `usePollCopy`. |
| 6.12 | Lock answers after sending | Built | — | New, not in classic. Premium marker: PO decision. |
| 10.3.2, 3.8, 3.14 | Editor switches: "unanswered counts as no", hide counts, time zone | Missing | Later | See 3.6, 3.8, 3.14. |
| 10.3.9 | Archive from inside the editor | Missing | Later | Dashboard only. |

### G · Dashboard, contacts, export, operations

| Ref | Function | Status | Needed for | Note |
|---|---|---|---|---|
| 7.1 | Dashboard: own and invited polls | Built | Run | The path `dashboard` is claimed, so a visitor who chose the new UI gets it. Shows the last list at once and refreshes it behind (cache). Guest-list card counts as classic. |
| — | Archive per user | Partial | Run | Backend built on 7 Oct (G18). Open: the new dashboard shows the button on every poll and sends an explicit state; the mobile app must be confirmed to call the route only while logged in. |
| 7.2 | Contacts: list, search, create, edit, delete | Built | Retire | A tab of the dashboard. Open: CSV export in the browser. |
| 7.3 | Contacts import (CSV, XLSX) | Built | Retire | |
| 7.5 | Excel export of a poll | Built | Run | Built in the browser per poll type; no cell colours (the free library cannot write formats). Compare with classic output in the audit. |
| 7.7 | Feedback form | Missing | Run | Backend exists (`POST /api/feedback`, public). **Needed as the feedback channel of phase 2** (section 6.1). |
| 7.8 | Chat (`ChatTest.vue`) | Decision | Later | Probably a test function. Check use. |
| 7.9 | Admin reports (`/admin/reports/polls`) | Decision | Later | Operations, not users. Decide whether this stays a classic admin page after retirement. |
| 7.4, 7.6 | Contact tags, poll print view | No | — | Not classic functions. |
| 7.10 | Emails | Backend | — | Unchanged. All links are `/pr/...`. |

### H · Links and routes

| Ref | Function | Status | Needed for | Note |
|---|---|---|---|---|
| 1.12, 9.5 | Old link formats (public key, legacy admin key, three-part link, invitation id) | Built | Run | Checked in a browser on 7 Oct. Open: an unknown second segment should go to the deleted page (H1) and the personal link should use the invitation id once the backend stops serving other participants' invitations (F8). |
| H1 | Deleted invitation or removed participant opens "poll deleted" | Missing | Run | Needs the invitation list in the poll answer; check the paged answer keeps it. |
| H2 | `/pr/new` (create a poll) | Built | Run | One address in both UIs: `pr/new` is claimed (8 Oct), so a visitor who chose the new UI gets the new panel there, **with or without an account**, as classic allows. Without a session the panel passes the admin key to the editor in the URL, and the first save sends name and e-mail as `ownerUser`, which moves the poll off the shared account (backend `reassignToUser`, unchanged). `/pr/newde` and `/pr/newen` stay classic (language from the path); `/new` redirects to `/pr/new`. Open: reloading the editor of a poll that is still type -1 opens classic ("deleted"); browser check of the anonymous flow. |
| H3 | `/vpr/:id/:key?` (old table view) | Decision | Later | No counterpart planned; decide whether it is dropped. |
| H4 | Claimed paths | Partial | Retire | Claimed: `dashboard`, `pr/new` and `new` (the new-poll page, 8 Oct). Exist but not claimed: `/membership`, `/membership/print-invoice-details`, `/unsubscribe`, `/deleted`, poll editor and share paths (served per poll type). Each is claimed after its audit. |

### I · Premium

How Premium is decided and enforced, and which functions are locked. The rule is the same everywhere: the poll creator's roles (premium bit or admin bit, also next to other bits) decide, with the visitor's roles only as a fallback (`frontend-v3/src/utils/premium.ts`, `User::isPremium()` on the server). Classic's own gates were taken from its code on 8 Oct; the four last rows are new rules decided by the PO that day, **not parity**.

| Ref | Function | Status | Needed for | Note |
|---|---|---|---|---|
| P1 | One Premium rule, in the new UI and on the server | Built | Run | The editor decided from `auth.user.plan`, the poll page from the creator's roles, the status chip from the visitor's. The server compared `roles != 1`. All use the one rule now. |
| P2 | Gates classic has and the new UI had: invite from other projects, own background images (3 free), tasks (10 free), sub-admin toggle | Built | Run | These were already locked. Images now use the shared rule instead of `plan`. |
| P3 | Gates classic has and the new UI lacked: an own logo, saving a custom design and "Speichern unter", the non-standard templates in the dropdown, the automatic reminders | Built | Run | Added 8 Oct. Going back to the standard logo and assigning a standard design stay free. A notice in place replaces classic's premium modal. Browser check open. |
| P4 | Classic markers without a check: seats per slot ("Je Termin"), allow tasks, booking mode, the sub-admin header, "lock answers after sending" (new in the new UI) | Decision | Later | Badge only, in classic and in the new UI. Left as they are; the PO decides with question 6 and question 11. |
| P5 | Hidden mode is Premium (new rule) | Built | Run | A free owner cannot switch it on. A poll that has it keeps it, and the owner can switch it off but not back on. Server checks `postDetails`, `postOptions` and `postNew`. |
| P6 | Excel export is Premium (new rule) | Built | Run | The whole export. The new UI builds the sheet in the browser, so its lock is advisory; the server route that classic uses is checked. "Sammelexport eigener Felder" does not exist in either UI: not built. |
| P7 | Invitations through kulibri: free up to 50 recipients, the poll's participants counted in (new rule) | Built | Run | Classic allowed 100. Blocks both invite panels and the server (`FREE_INVITE_LIMIT`, `app.free_invite_limit`). Conflicts with the product rule "functions that bring in participants are never priced": **PO to confirm** (question 11). |
| P8 | Messages to participants are Premium (new rule) | Built | Run | Not locked in classic. The panel opens and shows the notice. Server checks the poll owner. |
| P9 | Server enforcement | Partial | Retire | The four rules above are enforced (402). Open: image upload by subscription not roles, logo upload and designs unchecked, sub-admin not checked, grant/revoke rules differ. `SECURITY-BACKLOG.md`, SEC-014. |
| P10 | Classic follows the new rules | Built | Run | The server refuses with 402, so classic had to stop reporting those actions as done: the Excel export and messages to participants are gated in `Headline.vue` (a small Premium logo for a free account), hidden mode in `EditPollOptions.vue` and `EditKulibri.vue` (a free account cannot switch it on, keeps it where it is on), and the save, invitation and reminder panels show the premium dialog or the limit message on a 402 instead of the spinner or "sent". The free invitation limit in classic is 50 (`constants.freeInviteLimit`), as on the server. Helpers in `frontend/src/shared/premium.js`. **Browser check open** with a free, a premium, an admin and a premium+beta account, an admin-link visitor without a session, a grandfathered hidden poll, and copying a hidden poll as a free owner. Ships with the server change; do not deploy the server alone. |

---

## 6 · Plan

### 6.1 Phase 1: audit and readiness (before users are invited)

1. **Audit.** Compare every "Built" row marked *Run* with the classic app, on the same poll, owner and participant, signed in and anonymous, with `?classic=1`, at desktop and phone width, with the network panel open. Record the result in the row (Done, or a gap). Base the checks on the QA checklist of the rewrite (`docs/archive/rewrite-repo/docs/qa-pruefliste.md`, German). Lesson from the migration: mocked tests and screenshots alone missed three real defects; a logged-in check by a person found them.
2. **Close the *Run* gaps** in the checklist: language of the core poll screens (1.10), session expiry (419 retry), deleted invitation (H1), admin link meaning (5.5), the archive button on every poll, the feedback form (7.7), verification of custom participant fields (4.2).
3. **Feedback channel.** A "Send feedback" entry in the user menu and on the poll page that uses `POST /api/feedback` (public, sends a mail of up to 5000 characters) and adds the context automatically: UI version, path, poll type, browser, signed in or not. Also an easy way back ("Classic view") on every page.
4. **Support playbook.** One page: how to reproduce a report (headers `X-Kulibri-UI` and `X-Kulibri-UI-Reason`, `?classic=1`), the known gaps, and the safe way back (section 4).
5. **Measure.** Count how many users choose the new UI, how many switch back and where (the switch is one request), and the share of poll page loads per UI. Proposal; needs a small server-side counter.
6. **PO decisions** of section 7 for the first poll type.
7. **Switch on the first poll type** (`KULIBRI_UI_V3_POLL_TYPES=1`) on the test server, then production, after the audit of type 1 is done.

Exit: the points above are done for the first poll type; the PO agrees.

### 6.2 Phase 2: parallel run (up to three months)

- **Who:** users choose the new UI themselves ("Try the new design"); classic stays the default until phase 3. A short note on the switch tells what is not in the new design yet (those pages simply open in classic).
- **Feedback:** the channel of phase 1; weekly triage with the PO; every report becomes a row, a bug, or a decision; fixes ship in small branches.
- **Keep closing gaps** in this order of value: the remaining poll types (text, guest list, workshop, each after its audit and decision), then pages (membership, unsubscribe, deleted, editor), then the *Retire* rows. Each switch is a configuration change (a path in `v3_paths`, a type in `KULIBRI_UI_V3_POLL_TYPES`), with a test update where the resolver rules change.
- **Time box:** three months from the day the first poll type is offered to users. The end date is written here when phase 2 starts: ______.

### 6.3 Phase 3: retire classic

**Criteria to start** (proposal; the PO sets the numbers):
- every *Run* and *Retire* row is Done, or consciously deferred by the PO (with a name next to it);
- no open severity-1 bug in the new UI for two consecutive weeks;
- the feedback of the parallel run is reviewed and answered;
- the share of users who chose the new UI and stayed, and the share who switched back, is acceptable to the PO (number to agree);
- the backend items that block features are done or consciously accepted (section 6.5).

**Work in phase 3:**
1. Rebuild **sign-in, registration, email confirmation, password reset and Google sign-in** in the new UI on the session login (source and texts: the rewrite's auth pages in the archive; classic look first, as decided earlier). The Laravel session stays.
2. Claim all remaining paths; poll creation `/pr/new` in the new UI; decide `/vpr` and the admin reports.
3. Make the new UI the default; keep classic reachable with `?ui=legacy` for a grace period (proposal: four weeks).
4. Remove classic: `frontend/`, the Mix and vue-cli builds, the Ansible build steps, `UiVersionResolver` and the switch controls, `users.ui_version` (after the grace period), and the handover code. Update `CLAUDE.md`, the deploy role and the docs. Keep the old links working: the server then serves the new UI for every path.

### 6.4 Tracks that run alongside

**Structural work in `PollDetailView` (4,100 lines left).** The code of the large poll view is cut into composables and components, one branch per cut, with a characterisation test first and a browser check; merged one at a time into `vue3-redesign`. The risky pair (rows with answer editing) is done only with a browser check by a person and, ideally, together with the grid work.

| # | Cut | Status |
|---|---|---|
| 4 | Load and live sync (`usePollData`) | Done 7 Oct (also fixed a stale-response bug) |
| 1 | Visibility rules of hidden polls (`utils/pollVisibility.ts`) | Done 7 Oct |
| 8 | Poll theme (`utils/pollTheme.ts`, `usePollTheme`) | Done 7 Oct (screenshots identical before and after) |
| 5 | Participant administration (`useParticipantAdmin`, three dialogs) | Done 7 Oct (rendered HTML of five dialog states identical) |
| 11 | Copy and delete | Done 7 Oct: nothing to cut, the view already used the composables; page-level tests added |
| 10 | Comments and tasks tabs into components | Next |
| 9 | Guest list into `useGuestList` and components | Open; needs the own-row cut first |
| 7 | Invitation, share and QR state (`usePollSharing`) | Open; check duplicates with `InvitePanel` and `PollShareView` |
| 13 | Own row: who the visitor is, linked participants, the block in `loadPoll` that works it out (`useOwnParticipant`) | Open; was missing from the first list; cuts 6 and 9 depend on it |
| 6 | Submit step and email code step (`useSubmitAnswers`) | Open; after 13 |
| 2+3 | Rows (sort, search, filter, virtualisation) and answer editing (`usePollRows`, `useAnswerEditing`) | Open; the tangled core: every name is shared; done together by one person, ideally with the grid work |
| 12 | The template: moves with the cuts above; what is left is the frame | Open |

Then `PollEditorView` (2,200 lines), `CourseView` (900), `HomeView`, `ContactsTab`. **Done when** the architecture test's list of views that reach the transport directly is empty or consciously accepted, and the suite is green on five runs in a row. Rules: `docs/frontend-v3-rules.md`.

**Quality.** Frontend: 1,485 tests (Vitest), type check in the build. Backend: 465 tests on `vue3-redesign`, 417 on `master`. Each change adds a test that fails without it, and the rows in `TESTING-BACKLOG.md` are updated in the same change. There are no automated browser tests; the audit in phase 1 is manual. A scripted browser check for the main flows (sign in, open poll, answer, edit) would pay off during phase 2.

**New features after parity.** Grid and server-side paging (`GRID-KONZEPT.md`, written by Bolt), two-phase allocation for shift planning, address list, one-to-one appointments. Product order from the strategy: grid performance first, then workshops, address list, capacity primitive, shift planning, one-to-one, expense accounting. They are planned after the parity rows they depend on; the grid concept needs the "rows and answer editing" cut.

### 6.5 Backend and security items that touch this plan

| Item | State |
|---|---|
| G18 archive per user | **Built 7 Oct** (table `poll_user_archive`, per-user flag, route behind login). On `master` and `vue3-redesign`. **Run `php artisan migrate` after the deploy.** Open: new-UI button on every poll; mobile app check. |
| SEC-014 Premium on the server | **Partly fixed 8 Oct** (one rule `User::isPremium()`, 402 on hidden mode, Excel export, invitations over the limit, messages to participants). Open: image upload, logo and design routes, sub-admin, grant and revoke rules. |
| G19 an edited answer does not move `polls.updated_at` | Deferred (PO). |
| SEC-009 hidden polls: the server still sends other participants' answers | Open. Decides whether hidden polls can be served by the new UI (section 7). |
| SEC-012 unauthenticated helper routes | Four data-overwriting routes removed (merged into `vue3-redesign`, **not yet on `master`**); nine more to review. |
| SEC-013 archive and delete routes without login | Archive part fixed with G18; `DELETE /api/poll/{id}` still open. |
| F7 stage 2 "address already takes part" needs a code (409) | Open; blocks the new same-email dialog (9.2). |
| Email confirmation resend, `POST /email/resend` without a session | Open (G10, requirement B4); blocks nothing in phase 1. |
| Invitation lookup is not limited to the poll (`PollAccessResolver`) | Open finding; see `docs/route-map.md`, section 7. |
| Backend answer for a key it cannot decode is 500 instead of 404 | Open; the new UI works around it. |
| Backend change rule | Only after agreement with Mitko (`CLAUDE.md`). |

The full list of backend findings G1 to G19 (German) is in `docs/archive/rewrite-repo/docs/kulibri-backend-befunde_1.md`; their status is kept in `SECURITY-BACKLOG.md`.

---

## 7 · Decisions needed from the product owner

| # | Question | Proposal | Needed before |
|---|---|---|---|
| 1 | Which poll type first, and the order after that | 1 date poll, then 3 text poll, then 2 guest list, then 4 workshop (PO-16) | phase 1 exit |
| 2 | Default UI for visitors who have not chosen | Classic during the parallel run; the new UI becomes the default in phase 3 | phase 1 exit |
| 3 | Hidden polls | Stay classic until SEC-009 is fixed (the server then stops sending other answers) | first release of type 1 |
| 4 | Guest-list counting rule (PO-26) | "Without double counting", as classic shows | switching type 2 |
| 5 | Texts for account deletion and unsubscribe (PO-08, PO-11) | Confirm the proposed texts | first release to real users |
| 6 | Premium markers (2.4) and the logo lock (6.7) | Remove the markers without a function, keep the real ones | phase 2 |
| 7 | Retirement criteria (6.3), numbers and the grace period | Proposal above | start of phase 2 |
| 8 | English for the whole app (PO-17) | Core poll screens before phase 2; the rest before phase 3 | phase 2 / 3 |
| 9 | Chat, admin reports, old table view `/vpr` | Check use, then decide drop or keep | phase 3 |
| 10 | Invite by picking people from another project: (a) classic offers only participants whose address is **already in your contacts**, and silently leaves the others out — keep or drop that filter? (b) does the Premium lock stay? | (a) we propose keeping it for now and deciding with question 6; (b) keep the lock, it is a real one in classic | phase 2 |
| 11 | Premium scope, 8 Oct: (a) the free invitation limit is 50 where classic allowed 100 and the product rules say invitations are never priced: keep 50, go back to 100, or all kulibri-sent invitations Premium? (b) free users lose the Excel export and messages to participants, which were free: announce it, and from when? (c) existing hidden polls of free owners keep working: confirm. (d) the markers without a check (P4): remove, keep, or make real? | (a) 100 is classic and closest to the product rule; (b) announce first, switch on after; (c) yes; (d) remove the ones without a function | before users are invited |

---

## 8 · Deploy and environment

- **Branch and servers.** The test server deploys the branch set by hand in the Ansible repo (`branch:` in the `test` group variables). `master` is the stable line for backend fixes; `vue3-redesign` carries the new UI and is merged with `master` regularly.
- **Environment values** (Laravel `.env`, then `php artisan config:clear`): `KULIBRI_UI_V3_ENABLED`, `KULIBRI_UI_V3_POLL_TYPES`, `KULIBRI_UI_V3_HIDDEN_POLLS` (keep false), `KULIBRI_UI_V3_FEATURE_GUARD` (false only on test or after the fields are verified), `FREE_INVITE_LIMIT` (recipients a free account may invite, default 50). `.env.example` has comments.
- **Migrations are not run by the deploy role.** After a deploy run `php artisan migrate` and check `migrate:status`. Recent ones: `users.ui_version` (17 Sep), preferences `committed_at` and polls `options` as text (23 Sep), billing profiles and invoice issuing data (3 Oct), `poll_user_archive` (7 Oct, copies the old archive flag in about four seconds on a copy of production).
- **Build.** The new UI is built by `npm run build:v3` through the release scripts, which also write `public/v3` and `resources/views/v3.blade.php` (git-ignored). Check that the deploy installs the dependencies of `frontend-v3`: the role had no explicit step for it (state of 24 Sep, to confirm). Never run the root `npm run build` by hand: it bumps the version, commits and tags.
- **Local setup.** Use `http://localhost:8000` (not `127.0.0.1`), start the server with `XDEBUG_MODE=off`, set `MAIL_MAILER=log` on a copy of production data, and copy `public/images`, `public/legacy` and `resources/views/legacy.blade.php` (git-ignored). Tests use in-memory SQLite and cannot touch a real database.
- **Switch a path or poll type on:** the checklist of a claim: tests green, audit done, old links checked, owner and participant, signed in and out, one shared session, then the configuration value.

---

## 9 · Where things are

| What | Where |
|---|---|
| This plan | `V3-PARITY-PLAN.md` |
| Working rules for `frontend-v3` and how we test | `docs/frontend-v3-rules.md` |
| Instructions for Claude Code in this repo | `CLAUDE.md`, skill `.claude/skills/v3-parity/` |
| Links, link formats, second segment, route map | `docs/route-map.md` |
| Grid, sorting, inline editing, paging (concept) | `GRID-KONZEPT.md` |
| Theming and branding of a poll | `THEMING-AND-BRANDING.md` |
| Premium: scope, rules, limits, how a lock behaves, what the server enforces | `docs/premium.md` |
| Open design questions about links and invitations | `Design.md` |
| Test status and backlog | `TESTING-BACKLOG.md`, `SECURITY-BACKLOG.md` |
| History of the merge, the old plan, the rewrite's documents (German) | `docs/archive/` |

Translating the archived German documents that stay in use (first the gap board, the product owner questions and the QA checklist) is a separate step.
