# Integrating the Rewrite into frontend-v3: Direction, Decisions, Phases

Last updated: 6 October 2026 · Maintained by: Mitko (with Claude Code), PO

This document describes how the rewrite (`kulibri-rewrite`, Bolt's repo) will be integrated long-term into the `frontend-v3` folder of this branch (`vue3-redesign`). The status board for each screen is in `SCREENS.md`. Parity with the classic UI is tracked in `PARITY-GAPS.md` in the rewrite repo.

## 1 · Goal and Direction

The rewrite will be integrated into `frontend-v3`. The goal is **the same functionality as today, with a switch between classic and new UI**. The switch is the **existing same-link mechanism (SL, section 14)**: one link per poll, and the visitor's choice decides which UI opens it (`UiVersionResolver`, `config/kulibri_ui.php`).

**At migration start the rewrite is assumed correct (decided by the PO, 6 Oct 2026).** There is no separate sign-off gate. Issues found afterwards go through the usual bug process. Until the cut, bugs are fixed in the rewrite; from the cut on the code lives in `frontend-v3` and bugs are fixed there (never two live copies, section 9). Nobody from Bolt works on the code after the cut (section 15).

The question this document addresses: What must happen in the rewrite **beyond** closing parity gaps and fixing bugs to make integration simple and smooth? The code moves in **one cut**: minimal changes in the rewrite first (Phase A), a dry run to measure (Phase B), then all code is copied into `frontend-v3` and the standalone repository is frozen (Phase C). Whether paths and poll types are *switched* all at once or in steps is decided after the cut (Phase D).

The note in `CLAUDE.md` from 2.10. ("the rewrite replaces `frontend-v3`") is therefore outdated.

## 2 · Branch `vue3-redesign` and Folder `frontend-v3`

These are not the same. `vue3-redesign` is a **Git branch** (26 commits, 127 files vs. `master`, as of 3.10.). `frontend-v3` is a **folder** within it and accounts for 92 of those files. The rest is the switch for "two UIs at the same URLs":

| Area | Files | Content |
|---|---|---|
| `frontend-v3/` | 92 | the Vue 3 app |
| Backend | 10 | new: `UiVersionResolver.php`, `UiChoice.php`, `config/kulibri_ui.php`, migration `users.ui_version`; modified: `FrontendController`, `HomeController`, `UserController`, `EncryptCookies`, `routes/api.php`, `index.blade.php` |
| `tests/` | 4 | `UiVersionResolutionTest`, `UiPollRouteResolutionTest`, two Blade shells |
| `frontend/` (classic) | 10 | handover hooks (`router.js`, `services/uiVersion.js`), switcher in `NavUser.vue`, `Poll.vue`, translations |

None of this exists on `master`. The branch last merged `master` on 26.09. and is 22 commits behind (including fixes for renaming participants and creation without locale).

## 3 · Decisions

| Question | Decision |
|---|---|
| How the user chooses classic or new | **Same link (SL), decided 6 Oct 2026:** one link per poll, the visitor decides which UI opens it (`?ui=`, cookie, `users.ui_version`; default classic). Reason: the product owner wants the invitee, not the sender, to decide. The link-pattern switch (LPS) stays documented as the alternative (section 14). |
| How the rewrite code gets into `frontend-v3` | **One cut, never two live copies (6 Oct 2026):** minimal changes in the rewrite (Phase A, little remains: tests, handover, branch base), a dry run to measure (Phase B), then all code is copied into `frontend-v3` and the standalone repository is frozen (Phase C). Tool alignment, the session login and splitting the large views happen **after** the cut, in `frontend-v3`. |
| Login in the integrated version | **Session in both UIs (path C)**, which the existing switch already relies on: both UIs run on one host and share the Laravel session, and `users.ui_version` is read from the session user. The token login of the standalone rewrite is replaced at the cut. Session expiry is still open (section 5). |
| Overlap (Dashboard, poll date read view already exist in `frontend-v3`) | The rewrite replaces the v3 version **only after passing its tests and parity check.** This way, corrections found during the Dashboard port are not lost. |
| Aligning style and versions (`@/` alias, folder structure, Pinia 2 to 3, vue-i18n 9 to 11, Vite 5 to 6) | **First step after the cut, as separate commits** with the rewrite's tests as the safety net. Not before: it would collide with Bolt's daily changes. |
| Is session login acceptable? | **Yes (PO, 6 Oct 2026).** The two measures against early session expiry (section 5) remain work items. |
| Who works on the code after the cut? | **Not Bolt (PO, 6 Oct 2026).** The in-house team maintains `frontend-v3`; the standalone repository is archived at the cut. The schedule needs Bolt's last day (A6). |
| Is the rewrite correct at migration start? | **Assumed correct (PO, 6 Oct 2026).** Issues found later go through the usual bug process. |
| Which open gaps close before the cut? | **PO preference: as much as possible after the cut.** Proposal and the one exception in section 15, to be confirmed. |
| One design system or two? | **One: the rewrite's design tokens (PO, 6 Oct 2026, A-24).** `frontend-v3`'s old names (`k-*` palette) go; its screens adopt the rewrite's tokens or are retired (old dashboard and read view). The only visible shift is the error and decline tone: red instead of brownish orange, in screens that are replaced anyway. The rewrite's design tests stay as they are; UI-03, UI-04, UI-05 and UI-07 are decided once afterwards. |

## 4 · Login: Three Paths

*Update 6 Oct 2026: SL (section 14) keeps both UIs on one host with one session, so path C is the working decision. This section stays as the reasoning and as the fallback if LPS is dropped.*

**Starting point.** The classic UI works with session cookies (`withCredentials`, `/sanctum/csrf-cookie`, 401 and 419 are treated as "logged out"). `auth:sanctum` accepts both session and token. A token path exists (`Api/AuthController`: `POST /api/login`, `GET /api/me`, `POST /api/logout`; `POST /api/password/email` and `/reset`). Tokens never expire (`config/sanctum.php`: `expiration => null`). `frontend-v3` uses the classic UI's session and has no login pages.

**The pitfall on the same domain.** The domain is in `SANCTUM_STATEFUL_DOMAINS`, and the `api` group runs `EnsureFrontendRequestsAreStateful`. Every request with an `Origin` or `Referer` from this domain gets session and CSRF checking, even with a Bearer token. `VerifyCsrfToken::$except` is empty. Measured on 30.09.: `POST /api/login` with this origin returns 419; with any other origin, 200. For write calls with token, only login is affected. Since the switch serves both UIs at the same URLs, that's the case that matters.

| Path | What | Backend change required | Risk |
|---|---|---|---|
| **C: Session in both** | Login stays classic (`/signin`), rewrite login screens are dropped, account and billing pages work with session | none | low |
| **D: Bridge** | Classic UI stays session, new UI uses token, both on the same domain | exempt bearer requests from stateful check; endpoint "swap session for token"; guard fix; set `kulibri_ui` cookie on save | medium |
| **B: Token in both** | Classic UI also switches to token | like D, plus `email/resend` (uses the session guard), token expiry and rotation, remove domain from stateful list. `POST /api/register` and `POST /api/auth/google` are already built for the rewrite (see below) | high: production UI login for everyone at once, existing sessions end |

**Already built, but only on `master`:** Since 2.10., `POST /api/register` (with rate limiting) and `POST /api/auth/google` (Google login for token clients, merged in three steps) are there. On this branch and the test server they arrive only after merging `master` and deployment. These are points A1 and A3 in the list `docs/kulibri-backend-anforderungen-auth.md` in the rewrite repo. **Still open after my review:** A2 (login denies unverified accounts) and A4 (poll routes recognize the token user, that's the guard fix below). The rule "no backend changes before MVP or parity" has already been relaxed for login specifically.

**Additionally needed in every token path:**
- **Guard fix (G8):** `Auth::user()` uses the default guard `web` and doesn't see Bearer tokens. `getPoll` then returns empty `key` and `adminKey` and drops the owner's email, `/designs` returns only default designs. Draft: a small middleware with `Auth::shouldUse('sanctum')`, affecting 18 call sites.
- **Switch on page load:** The resolver reads `?ui=`, then cookie `kulibri_ui`, then `users.ui_version`. A page load in the browser carries no `Authorization` header, the third source fails for token users. Fix: also set the cookie when saving the choice.
- **Security:** A token in `localStorage` can be read by any script on the page; a session cookie is `httpOnly`. Plus, tokens don't expire.
- **Switching without re-login:** Both UIs are on the same origin and can read the same `localStorage` key.

Path C doesn't violate the rule "no backend changes before MVP or parity." Paths D and B need a consciously limited exception (login and guards, nothing domain-specific).

## 5 · Session Expiry: Why the Token Preference

The PO prefers token because the session expires often and re-login is required. Checked (read-only):

- Test server: `SESSION_DRIVER=cookie`, `SESSION_LIFETIME=120`, so two hours of inactivity. The default in code (`config/session.php`) is `env('SESSION_LIFETIME', 2)`, two minutes if the variable is missing. **The production value is unknown.**
- Login always sets `Auth::login($user, true)` (`LoginController::postLogin`), so "remember me" with a long cookie. After session expiry, you should silently be re-logged-in.
- The classic UI treats every 401 and 419 as "logged out" and immediately redirects to `/signin` (`frontend/src/shared/http.js`). `frontend-v3` does the same (`frontend-v3/src/api/http.ts`).

**Confirmed on 3.10. (Laravel behavior, replicated locally):** After session expiry, the CSRF token is invalid. A POST, PUT, or DELETE returns 419, even though the remember-me cookie would re-authenticate you on the next request. The UI sends you to login anyway.

**How verified:** Same Laravel code on a disposable server with its own SQLite database (nothing on test server, no real DB). After login there are three cookies (session, CSRF, remember). With only the remember cookie: write call **419**, read call **200** (silently re-authenticated), and after a new `/sanctum/csrf-cookie` the same write call **422** (authenticated, only the input was empty, nothing was written). **Repeated on the test server** (new test account, the old one was missing after cleanup `test-env:sanitize`): same result, write call 419, read call 200, after CSRF refresh 422. Also: **After logout the remember cookie is invalid** (401), so logout works. **Not verified:** the behavior of the Vue UI in the browser (only read the error handler code) and production config.

**Verification path in browser (for sign-off after the fix):** Log in to the classic UI, in dev tools delete only the session cookie (the `remember_web_…` cookie stays), then do a write action, like adding a comment. 419 and then the login page confirm the hypothesis. 401 means: the remember cookie is gone, the cause is something else (cookie domain, `APP_KEY`, encryption—check those).

**Two measures, both without token:**
1. **Check `SESSION_LIFETIME` on production:** The `Max-Age` of the cookie from `GET /sanctum/csrf-cookie` divided by 60 gives the minutes (test server: `Max-Age=7200`, so 120). **Only raise it if it's a few minutes there** (code default is 2). Not to days: sessions are stored as a cookie (`SESSION_DRIVER=cookie`) and can't be revoked server-side; a longer value extends the time a copied session cookie stays valid. The remember cookie, by contrast, is invalidated on logout. With measure 2, the session can stay short.
2. On 419, first fetch `/sanctum/csrf-cookie` and retry the request once; only redirect to login on the second failure. Small change in `frontend/src/shared/http.js` and `frontend-v3/src/api/http.ts`. It helps both UIs. **Status:** For the classic UI it's ready (branch `fix/csrf-retry-on-419` off `master`, commit `b39d9eba`, 19 Jest tests, not pushed, not verified in browser). For `frontend-v3` it's still open.

Token would turn "expires too fast" into "never expires" and needs the backend changes from section 4. If the PO still wants token after the two measures, Path D is the smallest step. **Decided 6 Oct 2026:** the PO accepts session login; the two measures stay as work items.

### What Now and What Later

**Now, because it's needed for every path or makes the decision possible:**
1. Replace the token transport with the session at the cut (Phase C). The dry run (Phase B) shows whether a seam in the rewrite first makes this cheaper.
2. Check and fix session expiry (two measures above). If the cause is fixed, the token preference may disappear before the decision is needed.
3. Implement the guard fix (G8). The rewrite currently uses token against the test server and bypasses G8 with its own storage. That's a bug, not a path decision.

**Not before the decision:** other backend changes for D or B (exempt bearer from stateful check, swap endpoint, token expiry, `email/resend`), port the classic UI to token, further work on login, signup, and Google screens in the rewrite beyond the standalone MVP (Path C drops them at integration).

**Timing:** The decision falls before Phase B because the trial run needs the auth adapter. The trial run uses the session adapter (Path C); the extra work for D is estimated separately.

## 6 · Rewrite Status (measured 3.10., `origin/main` 17ead39)

**Supports integration:**
- Components: 36 files, none import `api` or `http`, 28 depend on neither router nor stores.
- Utils: 14 files with no environment, router, or store dependencies.
- Only ten composables call the API.
- Architecture test exclusion list: nine views (was ten).
- 769 tests, type check and build pass. One test fails on fresh `origin/main` (`profilePanel.spec.ts`), not proven to be intermittent.
- `types.ts` differs from v3 in three lines, all in the rewrite's favor (course planner, waiting list). `pollKeyStore.ts` is identical.

**Complicates integration:**
- **Monoliths unchanged:** `PollDetailView` 4316 lines, `PollEditorView` 2201, `PollEditorPanel` 1078, together 63 direct API calls (was 55). New code follows the rules; old code was left untouched.
- **Login on token:** 20 files for login, signup, account, plus `stores/auth.ts`.
- **Paginated loading missing:** Neither `api.ts` nor `PollDetailView` know `paginate` or `/poll/{id}/{key}/preferences`. This is prerequisite 0 in `CLAUDE.md` (a 500×500 poll already crashed the view).
- **Style:** 259 relative imports, no `@/` alias (v3: 72 imports with `@/`). Versions: Vite 5 vs. 6, TypeScript 5.5 vs. 5.8, Pinia 2 vs. 3, vue-i18n 9 vs. 11.
- **Links:** Rewrite `/pr/:id/:participantId`, v3 `/pr/:id/:key?/:publicKey?`. `/pr/:id/bearbeiten` and `/teilen` are already the same in both.
- **Differences:** `resolveAssetUrl` with fixed host in rewrite, `sanitize.ts` extended in v3 (76 vs. 43 lines), response count in list (`confirmations + declines` in rewrite vs. `confirmations` in v3; classic UI shows both separately).

## 7 · Phase A: Minimum Changes in the Rewrite

*Revised 6 Oct 2026 for the one-cut plan (section 3). Sizes are estimates: S under half a day, M one to three days, L one to two weeks.*

**Principle.** Change in the rewrite only what is cheaper there or needed for the cut. Everything else happens after the cut in `frontend-v3`: in one place, with no second copy to keep in step, and without colliding with Bolt's daily changes.

| # | Item | State | Size | Done when |
|---|---|---|---|---|
| **A1** | Fix the flaky test | **Done 6 Oct** (`cbff71c`, branch `test/a1-a6-detail-view`, pushed, not yet merged). The cause is the closing transition of the profile panel; the test now waits for it. Not reproduced on demand, so it is a fix for the identified cause | done | 5 full-suite runs in a row passed |
| **A2** | *(dropped with the SL decision)* | The link-prefix constant (30 hard-coded `/pr/` in 6 files) is only needed if LPS is chosen later (section 14) | — | — |
| **A3** | Characterisation tests for `PollDetailView` | **Done 6 Oct** (`d872791`): 24 mounted tests and the harness `pollDetailKit.ts`; three deliberate breaks were caught. Text and course polls, live sync and the invite panel are not covered yet; they are added in Phase C before each split | done | the tests travel with the code at the cut |
| **A4** | Product owner decisions | Gate 1 (before migration) answered on 6 Oct 2026 except which gaps close before the cut; the questions for later gates are in section 15 | S, plus PO time | The gap decision confirmed; the rows for gates 2 and 3 added to `docs/po-fragen.md` |
| **A5** | Merge `master` into `vue3-redesign` and measure the gap (this repo) | **Done 6 Oct** (merge commit `5b2956ad`, pushed): 4 conflicts, all in text files (`.gitignore`, `CLAUDE.md`, the two backlogs), none in code; `routes/api.php` merged by itself. Backend **445 tests pass** (1,885 assertions), frontend-v3 **318 tests pass**, build passes. This branch's SEC-010 (archive and delete) was renumbered **SEC-013**, because `master` had used SEC-010 for the designs routes. `cut-prep/integration` merges onto the new base without conflicts. It also fixed the 500 on the paged answers request seen in the dry run | S–M | done |
| **A6** | Bolt's last day and final deliverables | Bolt does not work on the code after the cut (PO, 6 Oct 2026). Bolt's workspace has no Git and fell behind `main` once. The rewrite docs (`CLAUDE.md`, `REFACTORING-ROADMAP.md`: "Bolt sets the cut", `docs/ui-po-fragen.md`) assume Bolt continues | S, plus a talk | Agreed: the date of the last commit in the old repository, a list of what Bolt still delivers (design-dependent work, open visual questions), rewrite docs updated for the in-house team. Until then a sync check after each Bolt commit (`docs/SYNC-PRUEFUNG.md`) |
| **A7** | Whatever the dry run finds | unknown until measured | — | Items come back here only when they are cheaper before the cut than after it |

**Moved behind the cut (Phase C, in `frontend-v3`).** The earlier list of eleven items (6 Oct, morning) is replaced; where the rest went:

| Earlier item | Now |
|---|---|
| A2 same keys in `de` and `en` | Phase C, after the translation files are converted to `i18n/{de,en}.json` |
| A3 align common parts with v3 (`sanitize`, relative time, avatar, `resolveAssetUrl`) | Phase C, part of the alignment commits |
| A4 route map (1.12, 9.5, 1.8) | Phase C; it spans both UIs. Under SL it must be done before the first poll type is switched, because the new UI has to accept every classic link format |
| A5 separate the login | Phase C: replace the token transport with the session |
| A7 `usePollData` | Phase C |
| A8 decompose the large views | Phase C, with the cuts in section 9 |
| A10 docs and synchronisation | A6 above |
| A11 measure the gap to `frontend-v3` | A5 above |

**Exit criterion for Phase A** (trigger for Phase B): A5 and A6 done; A4 answered or consciously deferred; the rewrite suite green on 5 consecutive runs.

## 8 · Phase B: Trial Run (Dry Cut)

Step-by-step plan, time boxes and measurements: `REWRITE-DRY-RUN.md`. The result of the run is written into this section when it is done.

**Result (6 Oct 2026): go.** Full log with all numbers: `REWRITE-DRY-RUN-LOG.md`. The branch `dry-run/rewrite-cut` stays local until the real cut.
- **It works.** The rewrite's code runs inside `frontend-v3`: 1,262 of 1,274 tests pass (5 documented failures, 7 skipped), 0 type errors in source, the bundle builds in 7 s, and Laravel serves it through the existing resolver. A real poll from the local database renders with the rewrite's poll page inside v3's header; `?classic=1` shows the same poll in classic.
- **Tool versions are no problem** (Pinia 3, vue-i18n 11, Vite 6, TypeScript 5.8 broke nothing). **i18n merges cleanly** (105 shared keys, 1 differing value). **The session layer is already there:** v3's HTTP client is the rewrite's with the token removed; one real bug found in it (an empty 202 body threw).
- **The design system is the one real question:** the rewrite's tokens evolved and its specs enforce them; v3's 10 files (83 class occurrences) were mapped mechanically, 3 files still have raw literals.
- **Seen in code, as predicted:** the classic three-part link `/pr/{id}/{key}/{publicKey}` is not handled by the rewrite's poll page (route map A4, before the first switch).
- **Effort of the real cut: about 3 to 5 working days** (was 5 to 10), most of the uncertainty in the browser verification with a real session, which the dry run could not cover (logged-in flows, phone width). **Changes needed in the rewrite beforehand: none.**
- **Found in the first logged-in test (same day) and fixed in the dry run:** card images were loaded from `https://test.kulibri.com` (a prototype shortcut in `resolveAssetUrl`, plan item A3), and the dashboard cards used the full-size images (about 7 MB for the first 12 rows, previews exist). Fixed: same-origin image paths, `sm_` previews on dashboard cards with a fallback, 6 tests; 4 files, +88/-3, about 0.25 day to re-apply in the cut. The first fix caused a double-encoding bug that only a real page load showed. Details: `REWRITE-DRY-RUN-LOG.md`.
- **Two navigations on one page (third logged-in test), fixed in the dry run:** v3's header and the rewrite's own header were stacked, because the rewrite's routes did not set `meta.ownShell`. Fixed with `ownShell` on 10 routes, one shared `useClassicSwitch` composable and a classic entry in the rewrite's menu, 8 tests, about 0.25 day to re-apply. Still open (same-link task 2): visitors have no way to open the poll in classic from the rewrite's header.
- **Fourth logged-in test:** the answers request failed with 500 because the dry run's backend lacked the keyless routes of `master` (A5, not done yet; merging `master` fixes it: 4 text conflicts, 445 backend tests pass). Separately, **a public share link fails in the new UI** (the backend answers 500 to a public key, a legacy admin key or any unknown key; the classic client recognises those by pattern first). Fix and route table: `docs/route-map.md`; it must be done before the first poll type is switched.
- **Open before the real cut:** the browser verification and Bolt's last day (A6). PO-25 (what the dashboard counts) is answered, A-23: as in classic.
- **Found while answering PO-25:** the rewrite's guest-list card counts guests twice (`confirmations + guests`; the backend already includes the guests in `confirmations`). It is fixed in the cut, together with showing the participant count on date and text polls as classic does.

Trigger: Phase A done. In a **throwaway branch** of this repo (nothing in `vue3-redesign` changes):
- Import all rewrite code into `frontend-v3`. Try a git subtree import (keeps history and blame) against a plain copy.
- Align: imports to `@/` by script, Pinia 3, vue-i18n 11, Vite 6, TypeScript 5.8; session login adapter.
- Run the rewrite tests (1072 at 6 Oct), the v3 tests, type check and build, and measure what breaks.
- Open one poll end to end through the existing switch (`?ui=`, cookie, `users.ui_version`) with the shared session; check the handover to classic, `?classic=1` and the resolver's per-poll fallbacks.
- Check the deploy: how the servers build `frontend-v3` (the Ansible setup has no `npm install` task for it today), build time and Node versions.
- Check whether the rewrite's dashboard and date-poll view pass the v3 tests, to decide the folder layout (replace or coexist).

**Result:** (1) changes that are cheaper before the cut, which go back to Phase A; (2) the exact task list for Phase C; (3) go or no-go.

## 9 · Phase C: The Cut and the Work After It

All in `frontend-v3` on `vue3-redesign`, one branch per step, the suite green at every commit:

**Prepared (6 Oct 2026, local branches, not pushed):** the dry run is reusable as a vendor import plus an integration commit. `cut-prep/vendor-s0` holds the pristine copy of rewrite snapshot S0, made by `tools/rewrite-cut/import-rewrite.py` (idempotent, keeps a manifest, deletes what the rewrite removed, reports shell files to port by hand and missing dependencies); `cut-prep/integration` is that plus one integration commit and builds and tests like the dry run, and has since been tidied: **1,285 passed, 3 skipped, 1 failed** (the classic three-part link, route map A4), 0 type errors, `npm run build` passes. Both branches are on GitLab as a backup. At the freeze the script runs again on Bolt's final snapshot and the result is merged into the integration branch, so most of the work of steps 3 to 6 below is done before the freeze and only Bolt's last changes are merged and re-tested. Procedure: `tools/rewrite-cut/README.md`. Nobody develops on the prepared branches before the freeze (never two live copies).

**Update 7 Oct 2026, the freeze:** Bolt's last work is tagged `bolt-last` (rewrite repository, `0cc50e8`) and imported (`cut-prep/vendor-s1`, merged into `cut-prep/integration`, both on GitLab). Two conflicts and one script gap, all resolved (see the README); 1,326 tests pass, 1 fails (route map A4), 0 type errors, build passes. The grid and pagination handover is `GRID-KONZEPT.md` (English, on `master`) and `docs/kulibri-grid-uebergabe-dev.md` (German, on the rewrite branch `raster-paging-FE`).

**Status 7 Oct 2026 (evening): the cut is done, the structural work has started.** The integration is merged into `vue3-redesign` and pushed, with `master` merged in (the grid concept). Since then: the link formats of the route map (public key, legacy admin key, three-part link) work and are checked in a browser; the guest-list card no longer counts guests twice (A-23); polls with tasks are served in the new UI and the custom-participant-field guard has a switch (`KULIBRI_UI_V3_FEATURE_GUARD`, default on); the dashboard shows its cached list at once and refreshes it behind (`utils/pollCache.ts`); `usePollData` is extracted and fixes the stale-response bug (cut 4 below). The frontend suite has 1,385 passing tests (3 skipped), 0 type errors, and `npm run build` passes. The PO check of the first build is confirmed. The prepared `cut-prep/*` branches and the dry-run worktree have done their job and can be deleted.
1. **Freeze.** The last commit in the old repository is tagged (A6). Bolt does not work in this repo afterwards (PO, 6 Oct 2026).
2. **Base.** `vue3-redesign` contains `master` (A5).
3. **The cut.** All rewrite code is imported into `frontend-v3`. Layout as decided in Phase B. The rewrite's dashboard and date-poll view replace the v3 versions only after they pass the v3 tests. The switch controls ("Classic view" in the menu, "open in classic" on the poll page) and the handover, config and auth-guard code of `frontend-v3` stay.
4. **Alignment, separate commits:** `@/` alias, tool versions, translation files to `i18n/{de,en}.json` plus a de/en key-parity test, shared parts (`sanitize`, relative time, avatar, `resolveAssetUrl` reading an environment value).
5. **Login.** The token transport is replaced by the session (section 4, path C). The architecture test gets a second rule: token handling only in a closed set of files that can only shrink.
6. **Route map and handover.** A table "new path, classic path, entry in `v3_paths`, poll type, link key" and a test that every classic path has a new route or an explicit handover. Under SL the new UI must accept every classic link format (`/pr/{id}/{key}/{publicKey}`, the participant link) before the first poll type is switched.
7. **Old repository frozen.** Archived with a README pointing here. From now on bugs are fixed in `frontend-v3`.
8. **Then the structural work**, `usePollData` first (done), then the decomposition below. Characterisation tests are extended before each cut.

**Rules.** The cut and alignment commits contain no refactoring. No behavior change until all four are in. No path is registered in `v3_paths` before its Phase D check.

**Cuts for `PollDetailView`** (4,534 lines on 7 Oct 2026: 2,962 of script with 139 functions, about 1,570 of template). One branch per cut, tests stay green, no visible change, each cut merged on its own before the next starts. The plan was reviewed against the code on 7 Oct 2026. For each section of the script it was measured how many of the names it declares are also used elsewhere in the script (the coupling) and how large it is:

| # | Cut | Lines | Names shared with other sections | Notes |
|---|---|---|---|---|
| 1 | Hidden-poll and visibility rules into `utils/` (pure functions) | 54 | 8 of 11 | Cheap, and other cuts need it. |
| 8 | Theming into `usePollTheme` (check the overlap with `useTheme`) | 140 | 0 of 32 | Self-contained. |
| 5 | Participant administration (edit, delete, add) into `useParticipantAdmin` and three dialog components | 345 | 1 of 43 | The biggest clean block. |
| 11 | Copy and delete: drop the duplicates, use `usePollCopy` and `usePollDelete` only | 135 | 4 of 18 | **Done 7 Oct 2026, no code change needed.** The view already uses both composables; no duplicate is left anywhere (`createPoll` in `NewPollView` serves another purpose). What remained is about 35 lines of dialog state and navigation, which stay: extracting them would only add a layer. The one gap was that no page-level test covered them; `pollCopyDelete.spec.ts` (12) does now. The 135 lines the first count showed included unrelated functions of the neighbouring section. |
| 10 | Comments and tasks tabs into components with their composables | 218 | 5 of 30 | `CommentComposer`, `TasksTab` and `useTasks` exist. |
| 9 | Guest list (poll type 2) into `useGuestList` and components | 314 | 8 of 43 | Needs cut 13 first for the own row. |
| 7 | Invitation, share and QR state into `usePollSharing` | 209 | 8 of 40 | Check the duplicates with `InvitePanel` and `PollShareView`. |
| 13 | **Own row (new):** who the visitor is, the linked participants, and the block in `loadPoll` that works out the own row (its guest-poll "open the form" code is repeated four times) into `useOwnParticipant` | about 490 | 62 of 62 | Was missing from the first list. Cuts 6 and 9 depend on it. |
| 6 | The name/email submit step and the code step into `useSubmitAnswers` and a dialog component | 239 | 5 of 16 | After cut 13. |
| 2+3 | Sorting, search, past-date filter, day groups and grid virtualisation (`usePollRows`), together with row editing and draft answers (`useAnswerEditing`, including `saveChoicesChecked`) | 271 + 467 | 56 of 56 and 41 of 41 | The tangled core: every name is shared, so the two are cut together by one person. Highest risk (the grid once stayed empty until something redrew it); extra characterisation first. |
| 4 | Load and live sync into `usePollData` | | | **Done 7 Oct 2026** (`composables/usePollData.ts`, 20 tests plus 11 page-level tests). |
| 12 | The template | about 1,570 | | **Moves with the cuts, not at the end:** self-contained sections (admin dialogs, comments, tasks, guest list, share panel) become components in the branch of their cut. What is left at the end is the frame. |

The numbers in the list are the old numbering of this plan, kept so earlier references stay valid; the order to work in is the order of the table. Characterisation tests per cut: `pollDetail.spec.ts` covers opening, visibility, answering, the guest list, participant administration and comments; it does not cover theming, sharing and QR, copy and delete, tasks inside the page and the code step, so those get their test net before the move. Independent cuts (8, 5, 11, 10) may go to different developers at the same time, because they touch separate regions; merges go one at a time into `vue3-redesign` and each developer rebases first.

Then `PollEditorView` (2225 lines: settings, dates, participant fields, design, save), `CourseView` (902), `HomeView`, `ContactsTab`, and the small ones. **Exit criterion:** the architecture test's list of direct importers is empty or consciously accepted, and the suite is green on 5 consecutive runs.

## 10 · Phase D: Switching Path by Path

Decided after the cut, with measured numbers: all at once or in steps. The mechanism: poll links per poll type (`KULIBRI_UI_V3_POLL_TYPES`) and the visitor's choice, all other pages per path in `v3_paths`; visitors without a choice stay on classic until the PO decides otherwise. A path is only registered once `frontend-v3` fully handles it. Public poll links are switched last. Per step:
1. Create the route in `frontend-v3` and register the path in `config/kulibri_ui.php`, only once the feature is complete.
2. Verify: switching between classic and new, old email links, owner and participant, without login, one shared session.
3. Backend: only configuration, plus the resolver rules that hold polls back (tasks, custom participant fields, hidden polls), dropped one by one as the rewrite covers them, each with a test update.

## 11 · Verification

Per step in Phase A, per skill `kulibri-rewrite`:
- `npm test`, `npm run typecheck`, and `npm run build` in the rewrite with real exit codes, once with and once without `VITE_API_BASE_URL`.
- One counter-test per rule (break it on purpose, the right test must fail).
- Paginated loading also with a large test poll against the test server, read-only (network size and response time before and after), plus browser verification by the PO.
- Until the cut, after each Bolt commit, check that our kulibri files are intact.
- Phase D: `UiVersionResolutionTest`, `UiPollRouteResolutionTest`, and the handover test in `frontend-v3` stay green.

## 12 · Open Points

- Session expiry: verify the 419 hypothesis and check `SESSION_LIFETIME` on production (section 5).
- SL details: the default UI for visitors without a choice, when classic retires, hidden polls (SEC-009), the order of poll types (section 14). LPS stays documented as the alternative if the PO ever wants the sender to decide.
- Bolt's last day and final deliverables (A6). After the cut the in-house team maintains the code.
- Layout of `frontend-v3` after the cut and the fate of its overlapping screens (decided in Phase B).
- Deploy: how the servers build `frontend-v3` and with which Node version (Phase B).
- **Contradiction in the rewrite repo:** The shared decision doc (decision log from 2.10., marked ✓ for kulibri there), the login requirements list, `kulibri-backend-befunde_1.md` (finding D1) and the section "Wann wird die Zeile geschlossen?" in `PARITY-GAPS.md` still assume that the rewrite replaces `frontend-v3`, that token is the only login path, or that the large views are split before the port. They need to follow sections 1, 3 and 7 once A6 is agreed.
- **After deleting an account** the user lands on the classic sign-in. The new UI hands `?accountDeleted=1` over, but the classic sign-in does not know it (checked: no occurrence in `frontend/src`), so the notice "account deleted" is lost. Either add the notice to classic's sign-in (a small classic change) or accept it (PO-08 texts).
- **Rewrite documents move at the cut.** `PARITY-GAPS.md`, `docs/po-fragen.md`, `docs/ui-po-fragen.md`, `docs/qa-pruefliste.md`, `docs/requirements.md` and `docs/kulibri-backend-befunde_1.md` (findings G1 to G19) live only in the rewrite repository, which is archived. They hold decisions that exist nowhere else, for example the PO list of 7 Oct 2026 (dashboard cards, user menu, poll header, hidden poll tabs). They have to be copied into this repository (for example `docs/rewrite/`) before the archive, and the in-house team continues them there.
- **Backend, from Bolt's last findings:** G19 (an edited answer does not move `polls.updated_at`; deferred) and G18, now built (7 Oct 2026, branch `feat/archive-per-user`, merged into `vue3-redesign`): archive per user in a new table `poll_user_archive`, `GET /api/polls` sets `isArchive` per requesting user (both frontends unchanged), `PUT /api/poll/{id}/archive` needs a login and only works for polls on the caller's own dashboard (the archive part of SEC-013; `DELETE /api/poll/{id}` stays open), it toggles without a body and sets the state with `{archived: bool}`, and it no longer touches `polls.updated_at`. The migration copies the old shared flag, so every dashboard keeps what it shows today (checked on a production copy: 96,909 rows, 0 missing, 0 extra). **Deploy: run `php artisan migrate` right after the deploy, or `GET /api/polls` fails.** `polls.isArchive` stays untouched for one release. Still to do: the new dashboard shows the archive button on every poll and sends the explicit state (small frontend change), and someone who knows the mobile app confirms it calls the archive route only while logged in.
- **`CLAUDE.md` of this repo:** the paragraph "Frontend rewrite" still says later bugs are fixed in the rewrite. Update at the cut. The paragraph on poll links stays correct under SL.

## 13 · Which Gaps Close When (Classes A, B, C)

*Updated 6 Oct 2026 for the one-cut plan. The row-by-row table is in `PARITY-GAPS.md` (rewrite repo), section 11, "Wann wird die Zeile geschlossen?" (text still to be aligned, section 12).*

The classic UI stays the fallback: the switch works per path (`v3_paths`) and for poll links by the link pattern, and a path is only registered once `frontend-v3` fully handles it. An open gap therefore only keeps its own path or poll type on classic. Three points in time:

- **Class A, before the cut, in the rewrite:** only what is cheaper there: product owner decisions and small gaps already in progress. The decomposition of the large views, the paginated-loading composable, the login separation and the route map moved behind the cut (section 7).
- **Class B, after the cut in `frontend-v3`, before the affected path or poll type is switched** (Phases C and D): built directly in `frontend-v3`, in its translation format, with the real session and real emails. Examples: translating the remaining screens (1.10), old link formats (1.12, 9.5, 1.8; before the first poll type is switched), the same-email code step (9.2: built on the rewrite's `main`, waits for backend F7 stage 2; PO-21 is answered, A-18), the course planner (poll type 4 keeps classic links until it is done).
- **Class C, after the switch:** backlog while both UIs run. Examples: resend of the confirmation mail (needs a backend route), P2/P3 rows.

Browser comparisons ("prüfen" rows) are cheaper after the cut: the same poll opens in both UIs (`?classic=1`) with the same session.

## 14 · Same Link (SL): How Users Choose Classic or New

*Status: **decided 6 Oct 2026** (product owner: the invitee decides). Code facts checked on `vue3-redesign` (6 Oct 2026). Sizes are estimates, not measurements.*

**Goal.** A user can work in the classic or the new UI and switch between them. The hard part is poll and invitation links: they are sent by email, forwarded and bookmarked, and they must keep working.

### Options considered

| Option | How the UI is chosen | Verdict |
|---|---|---|
| **Same link (SL)** | One link per poll. The server picks the shell from the visitor's choice (`?ui=` > cookie > `users.ui_version`), per path (`v3_paths`) and, for `/pr/…`, per poll type (`KULIBRI_UI_V3_POLL_TYPES`) | **Chosen.** Already built; the invitee decides |
| Link-pattern switch (LPS) | The path: `/pr/…` is classic, a new prefix (for example `/poll/…`) is new, on the same host | Documented alternative below. Chosen only if the sender should decide |
| Separate host | The host | Two logins unless the session cookie is shared; every backend-built link needs the right host; no per-type fallback |
| One shell embedding the other | Inside one page | Vue 2 and Vue 3 on one page, style clashes; high cost for a temporary state |
| Replace at once | Nobody chooses | No choice, no gradual rollout, high risk |

**The deciding question** (put to the PO): when an owner who works in the new UI invites someone, which UI should the invitee see? Answer: the invitee's own choice, default classic. That means no backend change for mail links and no new link format.

### What exists today (on `vue3-redesign`, not on `master`)

- `UiVersionResolver` (220 lines): the new UI is served only if enabled, the path or poll type is listed, the visitor chose it, the poll is not hidden and has no tasks or custom participant fields; everything else is classic. `X-Kulibri-UI` and `X-Kulibri-UI-Reason` headers say why.
- Per-type go-live by environment value, no code deploy.
- The visitor's choice: `?ui=`, a cookie, and `users.ui_version` (`PUT /api/user/ui-version`).
- Switch controls in both UIs: "Try the new design" in the classic menu, "Classic view" in the new header, "open classic" (`?classic=1`, one-off) on the new poll page.
- Handover to classic for pages the new UI lacks (`LegacyPageView`, `handover.ts`).
- Tests: `UiVersionResolutionTest`, `UiPollRouteResolutionTest` and the handover tests in `frontend-v3`.
- Backend mail links need no change: they all link `/pr/…` and the server decides per visitor.

### What still needs doing for the rewrite

| # | Task | Size |
|---|---|---|
| 1 | Merge `master` into `vue3-redesign`, then deploy the branch (the resolver is not on `master` yet) | S–M |
| 2 | Put the two switch controls into the rewrite's UI: "Classic view" in the user menu and "open in classic" on the poll page. The pieces exist in `frontend-v3` (`links.ts`, `AppHeader`, `PollView`) and are kept at the cut | S |
| 3 | Resolver rules that hold polls back (tasks, custom participant fields): drop each when the rewrite covers the feature. Hidden polls stay classic until SEC-009 is fixed or the rewrite hides answers safely. Each change needs a test update | S each |
| 4 | **The new UI must accept every classic link format** (`/pr/{id}/{key}/{publicKey}`, the participant link), since under SL the classic links are the links. Parity rows 1.12, 9.5, 1.8, with the route map from Phase C | M |
| 5 | Switch each poll type on by environment value after its checks | config |
| 6 | Later, if the PO wants new as the default: an environment flag instead of the hard-coded "nothing chosen means classic" | S |

Roughly one to two working days beyond the integration itself (estimate), against three to six for LPS.

### What is not less work

- **Task 4 comes before the first switch.** Under LPS old `/pr/…` links stay classic, so the new UI would not have to understand them until classic retires.
- **What a visitor sees depends on state.** The same link opens different UIs for different people. Support reproduces a report with the headers and `?classic=1`.
- **Anonymous participants** only have a cookie per device. They see classic until the default is flipped, which is the safe behaviour during the transition.

### Reuse of the rewrite

All rewrite code is reused as code. Adaptations, small and isolated: the login transport (token to session), the switch controls, and accepting the classic link formats.

### Open decisions

1. **Default UI for visitors without a choice:** classic until the poll types are done, or new from day one. When to flip it (task 6).
2. **Hidden polls (SEC-009):** keep them classic or let the rewrite hide other participants' answers itself.
3. **Order of poll types** for the new UI (also PO-16).
4. **When classic retires** and what then happens to the switch (PO-12).

### Alternative: link-pattern switch (LPS), if the sender should decide

`/pr/…` stays classic and the new UI uses another prefix (`/poll/…` proposed), so a link carries its UI. The sender's choice then reaches invitees. Costs, from the code:
- **The rewrite hard-codes `/pr/` in 30 places in 6 files** (`router/index.ts` 6, `PollDetailView` 10, `PollShareView` 6, `PollEditorView` 4, `CourseView` 2, `HomeView` 2): a one-constant change.
- **The backend builds `/pr/` links in about 14 places** from `config('app.frontend_url')`: invitations and reminders (`PollController` 1133, 1368, 1422), new-answer and new-participant notifications (`ParticipantController` 45, `PollController` 1075, `Poll::pollAdminUrl()`), comment notifications (`CommentController` 96), deadline reminders (`PollReminderNotification`), exports (`PollExportService`, `PollController` 1495) and a hard-coded `https://app.kulibri.com/pr/…` in `AccessLogController`. They would go through one link builder with a per-poll setting, because many mails have no sender (a participant triggers a new-answer mail, a scheduled job sends deadline reminders). A backend change to agree first.
- A switch control per poll that rewrites the link, a mapping in the route map, and a redirect from `/pr/*` when classic retires.

Choosing LPS later costs no rework in the rewrite integration, and links already sent stay valid either way.

## 15 · Product Owner Decisions by Gate

*6 Oct 2026. Gate 1 is what the PO must answer before migration starts; the later gates are listed so nobody asks too early.*

### Gate 1: before the migration starts

| # | Question | Answer (6 Oct 2026) |
|---|---|---|
| 1 | Is session login acceptable? | **Yes.** The two measures against early session expiry (section 5) remain work items. |
| 2 | A freeze for the cut, and Bolt's role afterwards? | **Yes to the freeze. Bolt does not work on the code afterwards.** The in-house team maintains `frontend-v3`; the standalone repository is archived. Needed for the schedule: Bolt's last day (A6). |
| 3 | Which open gaps must close in the rewrite before the cut? | **Preference: as much as possible after the cut.** Proposal below, to be confirmed. |
| 4 | Who signs off that the rewrite is correct? | **Nobody.** At migration start it is assumed correct; later issues go through the usual bug process. |
| 5 | Does `frontend-v3` follow the rewrite's design tokens? | **Yes (A-24).** One design system for the new UI; see section 3. |

**Proposal for question 3.** Close after the cut, in `frontend-v3`: 9.2 (same-email code step: built on the rewrite's `main`, finished once the backend has F7 stage 2; PO-21 is answered, A-18), 1.10 (language, translated directly into `i18n/{de,en}.json`), 1.12, 9.5 and 1.8 (link formats, with the route map), 1.2 (resend confirmation mail, needs a backend route), 5.5 (admin link, to be clarified first) and every "prüfen" row (browser comparison with `?classic=1` on the same session). Reason: each of these is closed once, in its final structure, instead of in code that is about to be moved, aligned and split.

**The one exception follows from "not Bolt afterwards":** work that needs design input is cheaper while Bolt is still there. That is the open visual questions in `docs/ui-po-fragen.md`, and the course planner screens (3.4, 10.4.1 admin cancels a booking, 10.4.2 waiting list). Proposal: a time-box for Bolt on these before the freeze; whatever does not fit stays on classic, because poll type 4 keeps classic links until it is done anyway.

### Gate 2: before the first switch for real visitors

- Default UI for visitors without a choice, and when to flip it.
- Order of poll types (also PO-16).
- Hidden polls (SEC-009): keep them classic, or let the new UI hide other participants' answers itself.
- ~~Response count~~ answered (A-23): the dashboard shows what classic shows. Guest-list counting rule: PO-26, before guest-list polls are switched.
- Guest list "without double counting": before guest-list polls (type 2) are switched.
- Text confirmations (PO-08 account deletion, PO-11 unsubscribe): before the first release to real users.

### Gate 3: later

- PO-17, English for the whole app: when the remaining screens are translated.
- PO-12, when classic retires: before the last switch.
- Not tied to the migration: PO-01 to PO-07 (Premium, invoices, renewal), PO-09, PO-13 to PO-15 (security), PO-18 to PO-20.
