Put the Share form back outside #room, and pin it there
Found by oficina-14's adversarial review of 772c1e0. Both defects predate the rename; they arrived with the web/native-consistency branch. The member Share-link form moved into #controls, which is inside <div id="room" hidden>. #room stays hidden until room.js runs, so with JavaScript off the form did not render at all โ while the <noscript> paragraph immediately above went on saying "everything else on this page works without it". AGENTS.md makes the scriptless floor load-bearing with two named exceptions, and sharing a meeting link is not one of them. I verified it against history rather than taking it on report: at b6e8f29 the form sat at line 13, ahead of #room at line 15. What regressed was position, not presence. The form rendered either way, so every Contains-style assertion stayed green and nothing caught it. The new check compares the two offsets, and I confirmed it fails against the old layout before keeping it โ without that it would only have been a test that agreed with whatever the template happened to do. Also restores the comment deleted in c80a506, which was the only record that chat.js must load before room.js: both are deferred, deferred scripts run in document order, and that ordering is what puts window.Chat in place before room.js mounts it. Reorder the two script tags without it and chat silently never un-hides. GOTOOLCHAIN=go1.26.5 make ci passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 files changed,
+19
−1
internal/calls/templates/room.html+5 −1internal/callstest/room_test.go+14 −0
CI failed — run details
diff --git a/internal/calls/templates/room.html b/internal/calls/templates/room.html| index 8956f83..edfda3d 100644 |
| --- a/internal/calls/templates/room.html |
| +++ b/internal/calls/templates/room.html |
| @@ -18,6 +18,7 @@ |
| <p>You are {{.Name}}, joining as a {{.Standing}}.</p> |
| {{if .Floor}}<p rst-notice>This team's Calls has no relay yet. Calls usually connect only between people on the same network.</p>{{end}} |
| +{{if .IsMember}}<form method="post" action="/m/{{.Sqid}}/share" target="_blank" rel="noopener" data-busy="false"><button type="submit" rst-btn aria-label="Share meeting link">Share link</button></form>{{end}} |
| <noscript><p>The call needs JavaScript to reach your camera and the other participants; everything else on this page works without it. <a href="/m/{{.Sqid}}/join">Back to the holding screen</a>.</p></noscript> |
| <div id="room" data-sqid="{{.Sqid}}" data-boot="{{.Boot}}" data-self-name="{{.Name}}" data-is-member="{{if .IsMember}}1{{end}}" data-relay="{{if .Relay}}1{{end}}" data-peer="" hidden> |
| <div id="call-workspace" estilo-call-workspace="sidebar"> |
| @@ -29,7 +30,6 @@ |
| <button type="button" id="camera" rst-btn aria-label="Camera" aria-pressed="false">{{icon "video"}}Camera<span id="camera-state" class="calls-toggle-state" aria-hidden="true">On</span></button> |
| <button type="button" id="share" rst-btn aria-pressed="false">{{icon "monitor"}}Share screen<span id="share-state" class="calls-toggle-state" aria-hidden="true">Off</span></button> |
| <button type="button" id="devices-toggle" rst-btn aria-expanded="false" aria-controls="room-devices" hidden>{{icon "sliders-horizontal"}}Devices</button> |
| -{{if .IsMember}}<form method="post" action="/m/{{.Sqid}}/share" target="_blank" rel="noopener" data-busy="false"><button type="submit" rst-btn aria-label="Share meeting link">Share link</button></form>{{end}} |
| <button type="button" id="toggle-chat" rst-btn aria-expanded="true" aria-controls="chat" hidden>{{icon "message-circle"}}Chat<span id="chat-unread" aria-hidden="true"></span></button> |
| <button type="button" id="leave" rst-btn="danger">{{icon "log-out"}}Leave</button> |
| </div> |
| @@ -59,6 +59,10 @@ |
| to live somewhere that is not about to be overwritten. */}} |
| <p id="device-note" class="calls-device-note" role="status" hidden></p> |
| </div> |
| + {{/* The chat panel (spec ยง6): one partial, one script, one |
| + comment in partials/chat.html. chat.js loads before room.js in |
| + the head block, and two deferred scripts run in document order, |
| + so window.Chat is there when room.js mounts it. */}} |
| {{template "chat-panel" .}} |
| </div> |
| <div id="queue" class="calls-queue" hidden><h2>Waiting to join</h2><ul id="knocks"></ul></div> |
diff --git a/internal/callstest/room_test.go b/internal/callstest/room_test.go| index 707b1d2..c98f23e 100644 |
| --- a/internal/callstest/room_test.go |
| +++ b/internal/callstest/room_test.go |
| @@ -42,6 +42,20 @@ func TestRoomPageRendersItsHeaderStandingAndScriptlessFallback(t *testing.T) { |
| if headEnd < 0 || css < 0 || css > headEnd { |
| t.Fatalf("calls.css is not inside <head>: head ends at %d, calls.css at %d\n%s", headEnd, css, body) |
| } |
| + // The member Share-link form must sit outside #room, which is hidden |
| + // until room.js runs. It lived inside #controls between c80a506 and |
| + // 03588e2, which put a member action behind JavaScript while the |
| + // <noscript> paragraph directly above went on promising "everything |
| + // else on this page works without it". AGENTS.md makes that floor |
| + // load-bearing with two named exceptions, and sharing is not one of |
| + // them. Position, not presence, is what regressed: the form rendered |
| + // either way, so any Contains check would have stayed green. |
| + share := strings.Index(body, `action="/m/`+sqid+`/share"`) |
| + room := strings.Index(body, `<div id="room"`) |
| + if share < 0 || room < 0 || share > room { |
| + t.Fatalf("share form is not outside #room: share at %d, #room at %d\n%s", share, room, body) |
| + } |
| + |
| // The no-standing case belongs to |
| // TestRoomAndTakeoverAreGatedByStandingOrParticipation: this |
| // harness's member allowlist refuses to even mail a magic link to |