Let the app remember the last open page and reopen it (especially for the PWA) #186

Closed
opened 2026-09-05 15:54:14 +00:00 by Grandiras · 2 comments
Owner

This will be useful when we add more features and hierarchy.

This will be useful when we add more features and hierarchy.
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/460); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-186/run-460.md) — updating every ~5 minutes while the task runs.
Collaborator

Pushed claude/issue-186.

What it does

Ponente now reopens on the page you last had open. The manifest's start_url is /launch — an endpoint that redirects to whatever page this browser was last on, and to the default board if there is nothing worth reopening.

Services/LastPage.cs is the rule, and it is one method used in both directions: what may be written down is exactly what may be redirected to, so a hand-edited cookie cannot turn /launch into an open redirect (ReturnUrl.Sanitize does that half, the same way it does for a post-sign-in destination). It's a deny list — /login, /signup, /request, /access-denied, /not-found, /Error, /dev/… and /launch itself are doors and errors, not places — so the "more features and hierarchy" the issue anticipates get remembered without anyone coming back to this file. A denied page leaves the previous one standing, so a detour through sign-in doesn't cost you your board.

Decisions worth knowing about:

  • /launch, not a rule about /. / is the default board's own address and the wordmark links to it; a / that bounced you back to where you were would leave no way home and make a bookmark of the board lie. The second address is what separates open the app from open the board.
  • The manifest's id stays / — it identifies an installed app, so changing it alongside start_url would turn every existing install into a second icon rather than an updated one. icons.spec.js now pins both.
  • A cookie, not localStorage — the one thing in prefs-interop.js that isn't. The only reader is the server, deciding where to send a launch before it renders anything; kept where only the browser can see it, the feature would have to render the default board in order to leave it. A month's lifetime, where the language cookie gets a year: a language choice doesn't go stale, a location does.
  • Path only, never the query. Every query this app puts in a URL is either stripped the moment it's acted on (?card=) or belongs to a page that isn't remembered anyway.
  • MainLayout records it (first render + every LocationChanged), because every page renders inside it. 302 with Cache-Control: no-store, or a cached launch redirect would pin the app to the first page it ever saw.

Verified

  • dotnet build / dotnet test Ponente.sln — 477 passed, including the new LastPageTests (deny list, query stripping, //evil.example, header injection, self-redirect).
  • Ran the app for real (host Postgres + dotnet run) and drove it with Playwright: prefs.spec.js (4), icons.spec.js, install.spec.js (11 total), plus board.spec.js / changelog.spec.js as a regression check on the MainLayout change — 38 passed there, the single failure being share.spec.js asserting the compose stack's localhost:8090 origin, which a host-run app can't satisfy. No compose runtime in this container, so the suite wasn't run against the real stack.
  • The new e2e test walks the whole round trip: recorded on an in-circuit navigation, restored by /launch, /request deliberately not recorded (waiting on the circuit-built .handle-ghost first, so it asserts a cookie that wasn't written rather than one that had no chance to be), and / still going home.

docs/ui.md gets a Reopening where you left off section with the reasoning; orientation.md gets the service row and /launch in the endpoint list, frontend.md the prefs-interop.js note, and Changelog.Unreleased a bullet in both locales. One test change beyond the feature: DocumentationTests only knew routes mapped from string literals, so it now resolves one mapped through a constant — otherwise /launch would have read as a documented route the app doesn't serve.

No image: the change is a redirect, and a screenshot of the calendar would only show the calendar.


⏱ 17m 23s · 81 turns · tokens: 145.2k in / 54.7k out (+8137.1k cache-read)
📜 Full session transcript
→ Opened PR: #187

Pushed **`claude/issue-186`**. ## What it does Ponente now reopens on the page you last had open. The manifest's `start_url` is **`/launch`** — an endpoint that redirects to whatever page this browser was last on, and to the default board if there is nothing worth reopening. `Services/LastPage.cs` is the rule, and it is **one method used in both directions**: what may be written down is exactly what may be redirected to, so a hand-edited cookie cannot turn `/launch` into an open redirect (`ReturnUrl.Sanitize` does that half, the same way it does for a post-sign-in destination). It's a **deny list** — `/login`, `/signup`, `/request`, `/access-denied`, `/not-found`, `/Error`, `/dev/…` and `/launch` itself are doors and errors, not places — so the "more features and hierarchy" the issue anticipates get remembered without anyone coming back to this file. A denied page leaves the previous one standing, so a detour through sign-in doesn't cost you your board. Decisions worth knowing about: - **`/launch`, not a rule about `/`.** `/` is the default board's own address and the wordmark links to it; a `/` that bounced you back to where you were would leave no way home and make a bookmark of the board lie. The second address is what separates *open the app* from *open the board*. - **The manifest's `id` stays `/`** — it identifies an installed app, so changing it alongside `start_url` would turn every existing install into a second icon rather than an updated one. `icons.spec.js` now pins both. - **A cookie, not localStorage** — the one thing in `prefs-interop.js` that isn't. The only reader is the server, deciding where to send a launch *before* it renders anything; kept where only the browser can see it, the feature would have to render the default board in order to leave it. A month's lifetime, where the language cookie gets a year: a language choice doesn't go stale, a location does. - **Path only, never the query.** Every query this app puts in a URL is either stripped the moment it's acted on (`?card=`) or belongs to a page that isn't remembered anyway. - `MainLayout` records it (first render + every `LocationChanged`), because every page renders inside it. 302 with `Cache-Control: no-store`, or a cached launch redirect would pin the app to the first page it ever saw. ## Verified - `dotnet build` / `dotnet test Ponente.sln` — **477 passed**, including the new `LastPageTests` (deny list, query stripping, `//evil.example`, header injection, self-redirect). - Ran the app for real (host Postgres + `dotnet run`) and drove it with Playwright: `prefs.spec.js` (4), `icons.spec.js`, `install.spec.js` (11 total), plus `board.spec.js` / `changelog.spec.js` as a regression check on the `MainLayout` change — 38 passed there, the single failure being `share.spec.js` asserting the compose stack's `localhost:8090` origin, which a host-run app can't satisfy. No compose runtime in this container, so the suite wasn't run against the real stack. - The new e2e test walks the whole round trip: recorded on an in-circuit navigation, restored by `/launch`, `/request` deliberately *not* recorded (waiting on the circuit-built `.handle-ghost` first, so it asserts a cookie that wasn't written rather than one that had no chance to be), and `/` still going home. `docs/ui.md` gets a *Reopening where you left off* section with the reasoning; `orientation.md` gets the service row and `/launch` in the endpoint list, `frontend.md` the `prefs-interop.js` note, and `Changelog.Unreleased` a bullet in both locales. One test change beyond the feature: `DocumentationTests` only knew routes mapped from string literals, so it now resolves one mapped through a constant — otherwise `/launch` would have read as a documented route the app doesn't serve. No image: the change is a redirect, and a screenshot of the calendar would only show the calendar. --- ⏱ 17m 23s · 81 turns · tokens: 145.2k in / 54.7k out (+8137.1k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-186/run-460.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/187
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/Ponente#186
No description provided.