# Screens: Bolt to v3 status

Living tracker for each screen moving from the Bolt prototype (repo `kulibri-rewrite`, cloned outside this repo) into `frontend-v3`. Update a row in the same turn its state changes, not in a batch later.

Richtung, Entscheidungen und Phasen für die Übernahme des Rewrites als Ganzes stehen in `REWRITE-INTEGRATION.md`. Seit dem 3.10. gilt: Das Rewrite wird in `frontend-v3` integriert (nicht ersetzt), mit Umschaltung klassisch und neu.

## States

- **in progress**: lives only in the Bolt repo, Bolt is iterating, nothing in this repo yet
- **shared**: a straight copy in `frontend-v3/src/views/shared/` for demo and feedback, routed under `/preview/` with a "Preview" banner. Bolt may still change their copy. No i18n or refactor work, hardcoded strings stay. Every change from the original is marked `SHARED-COPY` in the file
- **adopted**: Bolt stops touching it. Final UI changes and refactoring happen here
- **landed**: routed, listed in `config/kulibri_ui.php` `v3_paths`, strings in `i18n/{de,en}.json`, live for users who chose v3
- **dropped**: not ported

Rules that go with the states:

- Strings are extracted into `i18n/*.json` when a screen **lands**, in the same commit as its route and `v3_paths` entry. Not earlier (churn), not later (retrofit).
- A "shared" review is read-only and covers: UI consistency with the `k-*` classes, hashid handling (ids are strings, never sorted or parsed), logic that belongs in `endpoints.ts`/`mappers.ts`, extra dependencies, contact with public poll links or participant data (needs its own discussion before adoption), and a rough estimate of the adoption work. Findings go in Notes, so they can still go back to Bolt while they own the code.
- Nothing in `v3_paths` before v3 fully implements the path. Public poll pages (`/pr/...`) go last.

## Screens

Bolt source paths are relative to `kulibri-rewrite/src/`. Line counts are as of Bolt commit `e6c91d8` (2026-09-26); the previous baseline was `31093cf` (2026-09-21). Bolt kept editing after screens landed here, see the working agreement below.

| Screen | Status | Bolt source | v3 path | Notes |
|---|---|---|---|---|
| Dashboard (projects) | **landed** (2026-09-24) | `views/HomeView.vue`, projects tab (520) | `views/DashboardView.vue`, `components/dashboard/*`, `components/AppHeader.vue` | Replaces the POC dashboard at `/dashboard`, for users who chose v3. Built from Bolt's design as small components (`PollCard`, `DashboardToolbar`, header) on the legacy dashboard's rules, which live in `src/dashboard.ts`. **Differences from the Bolt copy:** guest lists show `confirmations - guests` (Bolt added guests on top, counting them twice); "last answer" is now legacy's "by me 3 days ago" (it is `updated_at`, not proven to be an answer); dates and relative times follow the user's language; poll images stay on this origin (Bolt's mapper hardcoded `https://test.kulibri.com/`); archive is offered to owners only; poll types use legacy's four labels. **Not included:** copy-link (builds a bolt.host URL without the owner key, needs a decision on kulibri's `/pr/{pollId}/{publicKey}` scheme), and the Status and Rechnungen tabs (stubs). Contacts is a shared preview, see below. **Correction to the earlier shared-phase review:** Bolt's archive switch and its sort by `updated_at` both match legacy (`isArchive == checkbox`, default sort `updated_at`). The POC dashboard was the one that differed, not Bolt. Found on the way: **SEC-013** (was SEC-010 before the merge of `master`), archive and delete work without login **Bolt changed `HomeView.vue` again on 09-25 and 09-26 (card layout refactor, action icons) after this landed on 09-24. Those changes are not in v3.** |
| New poll | in progress | `views/NewPollView.vue` (205) | none | |
| Poll detail / answer grid | in progress, **next: date polls (Terminplan), read mode first** | `views/PollDetailView.vue` (4096, was 3248) | none | Largest screen. One component holds the date poll's three views (admin grid, participant columns, participant panel) plus the text, guest-list and course variants, so adoption splits it into components, as the dashboard was. Public poll link surface (`/pr/:id`, `/pr/:id/:participantId`), so needs a security discussion before adoption. Plan and decisions: "Terminplan (date poll)" below |
| Course planner (Kursplaner, poll type 4) | in progress | `views/CourseView.vue` (837) | none | New since the previous baseline. Slot cards with a capacity bar and a waitlist. The server owns capacity (`ParticipantController`: limits, `notAllowedChoiceList`, waitlist order by `committed_at`), so the screen must render the server's outcome and handle its rejection instead of deciding "full, so waitlist" itself. Not read in full yet |
| Poll editor | in progress | `views/PollEditorView.vue` (2119), `views/PollEditorPanel.vue` (1097), `components/WysiwygEditor.vue` (187) | none | Uses `utils/sanitize.ts` for rich text, review that before adoption |
| Poll share | in progress | `views/PollShareView.vue` (253) | none | |
| Invite panel | in progress | `views/InvitePanel.vue` (420) | none | Invitations touch the open questions in `Design.md` |
| Contacts | shared (2026-09-24) | `views/ContactsTab.vue` (368) | `views/shared/ContactsTab.vue`, route `/preview/contacts` | Was a tab of Bolt's Home. The landed dashboard has no Contacts tab yet, so it stays a preview page. CSV/XLSX upload (`POST /api/contacts/upload`). Only the import path changed. Not reviewed yet. Where Contacts lives in the new navigation is undecided |
| Login | dropped | `views/LoginView.vue` (174) | none | Token login. v3 uses the Laravel session, signing in stays at legacy `/signin` |

## Terminplan (date poll): read mode first

Decided 2026-09-26. Order: read mode, then editing, then invitation links, all on **existing polls**. Editing means, in this order: participants answering and editing their own row, then the owner's poll editor and admin tools. Invitation links (personal links, invitations, `visited`) come last.

How it is switched on:

- `/pr/{id}[/{key}[/{publicKey}]]` is one path for every poll type and both roles, so `UiVersionResolver` decides per poll. v3 serves it only for a visitor who chose v3, when the poll's `type` is listed in `KULIBRI_UI_V3_POLL_TYPES` (`1` = date poll) and the poll is not hidden. The value is empty by default, so nothing changes until it is set, and it needs no deploy. Set it only once the read view exists: until then a listed poll shows v3's fallback page with a link to the classic view.
- Read mode reaches only visitors who chose v3 (`?ui=v3`, the cookie, or their account), because a participant cannot answer in it yet. Everyone else keeps the classic UI at the same link.
- The admin URL (`/pr/{id}/{adminKey}`) is served as well, read-only, with an "Edit in classic view" button. Buttons like "answer / edit in the classic view" use `classicUrl()` (`?classic=1`): classic for that page only, no cookie or account change. The header's "Classic view" is the sticky switch.
- Hidden polls (`options.hiddenPoll`, about 9% of date polls in the local data) stay classic while `KULIBRI_UI_V3_HIDDEN_POLLS` is off. The server stopped redacting them on 2026-09-22 (SEC-009), so a frontend that receives the full list would have to hide it itself. Read mode must not decide that until SEC-009 is fixed.
- Owner designs: v3 shows the background image and the app theme only. In the local data about 1,500 date polls have a custom background image and 423 a non-default design; their logo, colours and grid opacity are a known difference.
- The grid must use the paginated API (`?paginate=1` and `GET /poll/{id}/{key}/preferences`), never the full payload: a 500 by 500 poll crashed it.
- Two answer scales that must not be mixed up: per option `preferences.value` (-2 maybe, -1 none, 0 no, 1 yes, 2 if need be, 3 waitlist) and per participant `participants.choice` (guest lists: -1 open, 0 no, 1 yes, 2 yes with guests).
- The app boots without a login (`meta.requiresAuth` is per route), so anyone holding a poll link can open it. The header offers Sign in and Classic view.

Steps:

| Step | Content | Status |
|---|---|---|
| 0 | Foundations: poll-link routing in the resolver, one-off classic link, boot without login, this plan | done 2026-09-26 |
| 1 | Read renderer: header, paginated grid, participants, comments, deadline and closed states, German and English, tests with fixtures shaped like real polls | not started |
| 1b | Parity check: v3's per-option totals against the server's own counts on a sample of real polls, plus screenshots next to the classic UI | not started |
| 2 | Editing: participants first, then the owner's editor and admin tools | not started |
| 3 | Invitation links | not started |

Found while doing step 0: legacy's `Poll.vue` rewrites a three-part link (`/pr/{id}/{key}/{publicKey}`) to `/pr/{id}/{publicKey}` with a full-page redirect that used to drop the query string, which would have thrown a visitor from `?classic=1` straight back into v3. It now keeps the query and hash.

## Working agreement with Bolt (proposed, not confirmed with them)

Goal: once a screen is adopted here, Bolt stops editing their copy and changes come to us. The exceptions are still to be named with the PO. A suggestion:

- Before adoption Bolt owns the screen fully.
- After adoption Bolt sends a change request or a pull request to this repo instead of editing their copy.
- `src/index.css` (the design tokens) stays shared, since colour changes ripple everywhere, with a rule for how it is synced.

Why it matters: the dashboard landed here on 09-24 and Bolt changed `HomeView.vue` four more times on 09-25 and 09-26. Their repo has no tests, so a re-port cannot be checked automatically.

## Open decisions

- URL map: Bolt's German segments (`/pr/:id/bearbeiten`, `/pr/:id/teilen`) versus kulibri's existing public link scheme `/pr/{pollId}/{publicKey}`. Only needed when a screen lands, since shared screens go under a preview prefix.
- Language: Bolt has German strings only and no English locale. Strings are extracted when a screen lands.
- Navigation: Bolt's Home had tabs (Projects, Contacts, Status, Invoices). The landed dashboard is the projects list only, so the classic UI is still the way to Contacts, membership and invoices. Decide the v3 navigation before Contacts lands.
- Per-poll appearance (custom logo, header colours, grid opacity) is a separate system from the app theme, see `THEMING-AND-BRANDING.md`.
- Locale for anonymous visitors: v3 defaults to German until the user is known. Legacy syncs the language from the WordPress cookie `wp-wpml_current_language`. Decide whether v3 does the same, or uses the browser language, before a public poll page goes live.
- Hidden polls: stay classic until SEC-009 is fixed (see above), or v3 hides other participants' answers itself, which is the fragile identity logic that caused SEC-009.

## Decided

- **Switching UI** (assumed 2026-09-24, confirm with the PO): the switch stays global. It is the user's `ui_version` and applies to every path v3 serves; the header has "Classic view" and the classic user menu has "Try the new design". A per-screen switch would need a preference per path. Revisit when a second screen lands.
- **First landing = the projects list only** (assumed 2026-09-24, confirm with the PO).
- **Preview prefix** (2026-09-24): shared screens are routed under `/preview/` and listed as one `preview/*` pattern in `config/kulibri_ui.php`. This is a deliberate exception to "nothing in `v3_paths` before v3 fully implements the path": only a visitor who chose v3 reaches it, and it never sits at a final URL. `UiVersionResolutionTest` asserts the shipped paths never claim poll, invitation or sign-in links, and `router.spec.ts` asserts every preview route stays under `/preview/`.
- **Brand colour** (2026-09-24): Bolt's orange `#F2760A` replaces the legacy coral. Revisit if the frontend team did not choose it on purpose.
- **Theme** (2026-09-24): `frontend-v3` uses Bolt's design system (`src/index.css`, `tailwind.config.js`). Light by default, dark through a header toggle, remembered in `localStorage`. This is the app shell only; a poll page takes its look from the owner's design.
- **Order of screens** (2026-09-26): Terminplan (date poll) first, read mode, then editing, then invitation links, on existing polls. See "Terminplan (date poll)" above.

## PoC: Bolt's poll page taken over (2026-09-26, date polls, local only)

Copied from the Bolt clone at `31093cf` (`C:\MyProjects\05-Laravel\kulibri-rewrite`, older than the `e6c91d8` baseline above; to be refreshed when Bolt has committed everything) into `frontend-v3/src/views/poll/` (`PollDetailView`, `PollEditorView`, `PollEditorPanel`, `InvitePanel`, `PollShareView`), `components/WysiwygEditor.vue`, `composables/usePollSync.ts`, `pollKeyStore.ts`. It is now the poll route itself (`poll` in `router/index.ts`, no flag; the `?bolt=1` switch of the first PoC run is gone). The page written for v3 earlier (`views/PollView.vue`, `src/poll/*`) is no longer routed and stays until its rules and tests are carried over. Nothing else is switched off: the copied answer flow is live.

Changes to Bolt's code (each marked `SHARED-COPY`, to be re-applied on every Bolt update):

| File | Change | Why |
|---|---|---|
| all copied files | imports `../api`, `../http`, `../mappers`, `../stores/auth`, ... rewritten to `@/...` | different folder layout |
| `PollDetailView.vue` | `clearToken()` replaced by `auth.logout()` + full page load | v3 uses the Laravel session, not Bolt's bearer token |
| `PollDetailView.vue` | `back()` uses `window.location.assign('/dashboard' or '/signin')` instead of route names `home`/`login` | v3 has no such routes |
| `PollDetailView.vue` | `startSync()` (20 s polling of `getPoll`) not called | `getPoll` deletes temp participants and fires analytics on every call |
| `links.ts` | `publicBaseUrl()` added | used by the copied views |
| `i18n/{de,en}.json` | Bolt's `poll`, `admin`, `setup`, `join`, `common` strings added (German in both files, only missing keys) | copied views use them; English still to translate |

Known gaps found by the PoC (not fixed): Bolt's page still reads `route.params.participantId` as the URL segment, so admin keys and personal-link ids in `/pr/:id/:key?/:publicKey?` are not passed on (the owner was recognised only because they were signed in); Bolt's own top bar (logo, Edit, Invite, Log out) doubles v3's `AppHeader`; Bolt's own routes (`/pr/:id/bearbeiten`, `/teilen`, participant route) do not exist here and clash with the key segment; the poll takes several seconds to appear on the single-threaded `php artisan serve`. Checked in headless Edge on `:8000`: the owner sees the Bolt grid with real data (tabs, sticky header, totals, search, sort, legend); an anonymous visitor gets the page with the local "Deine Antwort" row (answering not tried).

### PoC follow-up (2026-09-26): routes, answering, editor check

- Routes added in `router/index.ts` before the poll route: `/pr/:id/bearbeiten`, `/pr/:id/:segment/bearbeiten`, `/pr/:id/teilen`, `/pr/:id/:segment/teilen` (Bolt's editor and share views). The v3 handover guard leaves moves inside one poll (poll, editor, share of the same id) in the app; every other poll link still goes through the server (`handover.spec.ts`, 3 tests).
- More `SHARED-COPY` changes: after a submitted answer Bolt did `router.replace({ name: 'poll-detail-participant' })`, a route that does not exist here; it threw, `loadPoll()` never ran and the own row stayed empty next to a duplicate. Now `router.replace({ name: 'poll', params: { id, key: newPid } })`. Two `router.push({ name: 'home' })` in `PollEditorView.vue` became `window.location.assign('/dashboard')`.
- Checked in headless Edge on `:8000`, anonymous visitor on a public link: pick answers, "Antwort abschicken", name, submit. Participant and preferences (1, 1, 0) are saved, the owner mail is queued, the success dialog with the personal link appears, the address becomes `/pr/{id}/{participantId}` and the own row shows the saved answers. Owner with admin link: the admin panel opens, `/pr/{id}/{adminKey}/bearbeiten` opens Bolt's editor with the poll's data.
- Not yet checked: "Antwort ändern" through the personal link after a reload, saving an edit in the editor, invite by e-mail, the share page, comments, guest lists. Observed in the editor and not explained: the title reads "Neue Umfrage anlegen" although the poll exists, and several time slots appear twice (9 and 16 October; the grid shows each date once), so saving from the editor must not be tried on real polls before this is understood. A `TypeError ... 'status'` is logged on the poll pages (also seen earlier).
