Repository source: docs/plans/ctrl_enter_late_signing/ctrl_enter_late_signing_one_shot.md
Ctrl+Enter after late signing load — one-shot
Cycle: v3/ctrl_enter_late_signing; Mode: one-shot; Record/index: this file, plans index
Debug mode: enabled (log; project .railspike_debug_on marker present)
Effort study: railspike-effort / v3/ctrl_enter_late_signing, standing enrollment under protocol P2, reference R1 (ledger)
Extended estimates: enabled (new-cycle default; one approach, no unit estimates)
Cycle estimate: 0.08
Status: complete. Merged in #73 (1b3de824), deployed, and accepted by the maintainer on zenmemes.com, 2026-11-08.
Request and authorization
Feedback item: PF-20261108-B02, created for this cycle (no existing item matched; PF-20261104-F68 covers Ctrl+Enter from Title and tags, a different change).
Original request, 2026-11-08 (https://zenmemes.com/threads/thread-20261108164027-d91d0c9c, by thaks): "ctrl+enter has stopped working on the board page's compose form #bug #regression".
Authorization, 2026-11-08: the maintainer pasted the thread link into a session. Under thread to pull request the paste authorizes a one-shot cycle through an open PR and a reply in the thread. Exclusions: merge, deployment.
Outcome and work type
Bug fix (client script). For an approved member, Ctrl+Enter in the board composer submits the post again.
Cause
browser_signing.js set itself up only on DOMContentLoaded. Since d9a0b0a1 (2026-10-29, messaging loaded on demand), member_approval.js fetches it for approved members together with the messaging scripts, and that fetch can land after DOMContentLoaded. The script then never initialized, so the board composer was never bound and had no Ctrl+Enter handler. The lazy composer loader (lazy_compose_signing.js) did not recover: it found the signing tag already in the page, treated it as loaded, called init before the script had run (a no-op) and cached that result.
Evidence from the reporter's browser (Chrome on Windows, approved member, page served by the service worker): DOMContentLoaded at 916 ms; browser_signing requested by script at 940 ms, ending 943 ms; the only signing tag was the dynamic one; ForumBrowserSigning loaded but the compose root had no data-browser-signing-bound; the keydown reached the form unhandled (defaultPrevented=false). Calling ForumBrowserSigning.init(document) by hand bound the composer and Ctrl+Enter worked.
Approach and reuse decisions
Initialize when loaded late. At the end of
browser_signing.js, callinitBrowserSigning(document)immediately whendocument.readyStateisinteractiveorcomplete; otherwise keep waiting forDOMContentLoaded. Binding is already idempotent (data-browser-signing-boundper compose root), and pages that include the script statically already get a secondinitfrom the lazy loader on focus, so this adds no new double-setup path.Considered and not done: making the lazy loader wait for a present-but-unexecuted tag. It would fix only the lazy path; any other script that loads signing late would still leave it uninitialized. The fix at the source covers every loader.
Considered and not done: dropping
browser_signing.jsfrom the messaging script list. The messaging scripts need it, and the order matters.No new dependency, markup, CSS, API or copy.
Concepts touched
None in the concept inventory.
Risks
scheduleIdentityPrewarmandscheduleAutomaticGuestIdentitynow also run on pages where an approved member's messaging scripts load signing late. Approved members already have an identity, and these already run on every page that includes signing. Accepted.
Verification and recovery plan
New
BrowserSigningNormalizationTest::testLateLoadBindsComposerCtrlEnterWithoutDomContentLoaded: withreadyStatealreadycompleteand no furtherDOMContentLoaded, the compose root is bound and Ctrl+Enter requests the primary submit. Fails onmain(Expected '1' but got ''), passes with the fix.Browser replay in headless Chrome against zenmemes.com with POSTs blocked.
Recovery: revert the commit.
Starting revision and branch: d1052b3ff716c55b8d6bb53118d00aa585294b0e on main, rebased onto ef72ce2b before the PR; branch fix/ctrl-enter-late-signing.
Study history
Intake, 2026-11-08T16:45Z, 0.05. Single exposed rater: Claude (claude-opus-5-5). Pristine intake missing: the keyboard handler was read before rating. Scope v1: restore Ctrl+Enter in the board composer. Compared with R1's duplicate preview suppression (0.10).
Execution entry, 2026-11-08T17:05Z, 0.08. Revised up: the cause could not be reproduced in a fresh, anonymous or simulated approved session and needed diagnostics from the reporter's browser.
Closeout, 2026-11-08. Projection 0.08 reaffirmed; retrospective scope assessment 0.08, same single exposed rater. Corrections: none. Additions: none. Delivered scope v1 in full. Maintainer accepted after verifying on zenmemes.com. No active-work measurement collected.
Dated execution history
2026-11-08: replayed in headless Chrome locally and against production (fresh, typed immediately, saved key, all 15 themes, simulated approved member, offline); all submitted. Reporter's browser diagnostics located the unbound composer and the late signing load.
2026-11-08: record and feedback item created; fix and test implemented.
2026-11-08: merged in #73 and deployed; the maintainer confirmed Ctrl+Enter works on zenmemes.com.
Outcome
Delivered as bounded. Limitations: the exact load interleaving in the reporter's browser was not reproduced headlessly; the fix was checked against its observed state.
Verification
Passed:
./v3 test BrowserSigningNormalizationTest, 84 run, 84 passed (includes the new test)../v3 test --smoke BrowserSigningNormalizationTest: all passed exceptBoardListTest::testPinnedThreadsComeFirstAndLatestThreadsArePaged, which fails identically onmainwith this change stashed (pre-existing, unrelated).Headless Chrome, zenmemes.com, POSTs blocked: with the fixed script served in place of production's and the signing script appended after
DOMContentLoaded(asmember_approval.jsdoes), the composer is bound before any interaction and Ctrl+Enter prevents default and fires submit, at 1280px and 390px.Not reproduced: the reporter's exact interleaving (headless Chrome either held
DOMContentLoadedfor the messaging chain or let the lazy loader add its own copy first, and both bound on production too). Checked against the reporter's recorded state instead.Not run: the full suite (maintainer runs it). Not checked: Firefox and Safari.