Repository source: docs/plans/archive/forte_board_reply_restore_step3_development_plan.md
Forte Board Reply Restore — Step 3: Development Plan
Stage 1
Goal: Make the compose panel's
return_tofield track the current tag filter and selected thread instead of the static/forte.Dependencies: none (builds on already-shipped
forte_reply/forte_board_reply)Expected changes:
public/assets/paned_board_reader.js—return_to's value is recomputed (via the same query-string construction already used byurlForState(), not a hand-rolled duplicate) whenever selection changes (selectThread), selection clears (resetContentPane), or the tag filter changes (selectFolder), so it always reflects the livetag/selectedstate at submit timeVerification approach: headless-browser test — apply a tag filter, select a thread, read the compose panel's hidden
return_toinput value and confirm it contains both the current tag and the selected thread's idRisks or open questions:
Reusing
urlForState's query-building logic (rather than a second implementation) so the two never drift apart
Canonical components/API contracts touched:
public/assets/paned_board_reader.js(extended)
Stage 2
Goal: Let the server accept and forward this richer board return path safely.
Dependencies: Stage 1
Expected changes:
Application.php::resolveComposeReplyReturnTo(string $requestedReturnTo, string $threadId): stringextended so a/fortereturn target may carry a query string restricted to justtagandselected(each validated against a narrow, character-restricted pattern, same spirit as the existing ASCII-token checks used elsewhere); anything outside that shape still falls back to the current safe defaultsVerification approach:
curl -i -X POST /compose/replywithreturn_to=/forte?tag=general&selected=root-001→ redirect Location matches exactly; malformed/foreign query content (e.g. an extra unexpected param, or non-token characters) falls back to plain/forteor classic/threads/{id}, confirming the guard still holdsRisks or open questions:
Keep the allowlist conservative (
tag,selectedonly) so this doesn't become a general-purpose open redirect surface
Canonical components/API contracts touched:
Application.php::resolveComposeReplyReturnTo(extended)
Stage 3
Goal: Restore tag filter and thread selection on page load when the URL carries them.
Dependencies: Stage 2 (so the query params actually arrive after a reply redirect)
Expected changes:
paned_board_reader.js's startup logic readsselectedfrom the URL (tag restoration already works today viaresolveForteBoardTag()'s existing server-sidetaghandling) and calls the existingselectThread()if that id matches a rendered thread; otherwise leaves the default "nothing selected" placeholder state untouchedVerification approach: headless-browser test — load
/forte?tag=general&selected=root-001directly and confirm that thread is selected with Reply enabled on load; load with a nonexistentselectedvalue and confirm no error, default state holdsRisks or open questions: none identified
Canonical components/API contracts touched:
public/assets/paned_board_reader.js(extended)
Stage 4
Goal: Verify the full round trip and confirm no regressions on adjacent flows.
Dependencies: Stages 1-3
Expected changes: none expected (integration/regression verification only)
Verification approach: headless-browser test covering: filter by tag → select thread → open composer (confirm
return_toreflects both) → submit → confirm redirected page loads with that same tag filter and thread selected, Reply enabled; separately confirm submitting with no filter/no selection still lands cleanly on plain/forte(Step 2's stated edge case); separately confirm the single-thread reader's/threads/{id}/fortereturn path is unaffectedRisks or open questions: none identified
Canonical components/API contracts touched: none new — integration check across Stages 1-3, plus regression check on the untouched
/threads/{id}/fortepath