oficina / meet Public

holding page: standing-gated, scriptless, says the room is next

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 47c19f3e8cd9d5ca007fe4b70cb487ffde551184 parent f189d0f
4 files changed, +82 −2
  • internal/meet/app.go +1 −0
  • internal/meet/handlers_join.go +32 −0
  • internal/meet/templates/join.html +10 −2
  • internal/meettest/holding_test.go +39 −0
diff --git a/internal/meet/app.go b/internal/meet/app.go
index 3ec24e8..a08d8a2 100644
--- a/internal/meet/app.go
+++ b/internal/meet/app.go
@@ -85,6 +85,7 @@ func build(d *db.DB, cfg Config) (*http.ServeMux, *app, error) {
r.Use(a.requirePerson)
r.Get("/continue", a.continueSignedIn)
r.Get("/welcome", a.welcome)
+ r.Get("/m/{sqid}/join", a.holding)
r.Group(func(r chi.Router) {
r.Use(a.requireMember)
r.Get("/", a.index)
diff --git a/internal/meet/handlers_join.go b/internal/meet/handlers_join.go
index 80bcb2c..e0b960a 100644
--- a/internal/meet/handlers_join.go
+++ b/internal/meet/handlers_join.go
@@ -41,3 +41,35 @@ func (a *app) followLink(w http.ResponseWriter, r *http.Request) {
}
http.Redirect(w, r, "/m/"+m.Sqid+"/join", http.StatusSeeOther)
}
+
+// holding is GET /m/{sqid}/join, authorized by standing (spec §2.5). In
+// this slice it renders the server-side half of the holding screen and
+// says the room is the next slice.
+func (a *app) holding(w http.ResponseWriter, r *http.Request) {
+ m, ok, err := a.store.meetingBySqid(r.Context(), chi.URLParam(r, "sqid"))
+ if err != nil {
+ a.logger.Error("meet: load meeting", "err", err)
+ ErrorPage(w, r, http.StatusInternalServerError, "")
+ return
+ }
+ if !ok || m.EndedAt != nil {
+ ErrorPage(w, r, http.StatusNotFound, "")
+ return
+ }
+ p, _ := currentPerson(r)
+ st, err := a.standingFor(r.Context(), p, m)
+ if err != nil {
+ a.logger.Error("meet: standing", "err", err)
+ ErrorPage(w, r, http.StatusInternalServerError, "")
+ return
+ }
+ if st == StandingNone {
+ ErrorPage(w, r, http.StatusNotFound, "")
+ return
+ }
+ render(w, "join", map[string]any{
+ "Sqid": m.Sqid, "Title": a.store.title(m), "Starts": a.fmtStart(m.StartsAt),
+ "Name": p.Name, "Standing": st.String(), "IsMember": st == StandingMember,
+ "Floor": !a.cfg.RelayConfigured,
+ })
+}
diff --git a/internal/meet/templates/join.html b/internal/meet/templates/join.html
index 8326cd1..ad0872e 100644
--- a/internal/meet/templates/join.html
+++ b/internal/meet/templates/join.html
@@ -1,2 +1,10 @@
-{{define "title"}}Meet{{end}}
-{{define "content"}}{{end}}
+{{define "title"}}{{.Title}}{{end}}
+{{define "content"}}
+{{if .IsMember}}{{template "back-nav" dict "Href" "/" "Label" "Meetings"}}{{end}}
+{{template "page-header" dict "Title" .Title}}
+{{if .Starts}}<p>Starts {{.Starts}}.</p>{{end}}
+<p>You are {{.Name}}, joining as a {{.Standing}}.</p>
+{{if .Floor}}<p rst-notice>This team's Meet has no relay yet. Calls usually connect only between people on the same network.</p>{{end}}
+<p>The call itself is the next slice of this app. When it lands, this page will show your camera preview, device pickers and a join button, and needs JavaScript for that part only; everything else here works without it.</p>
+{{if .IsMember}}<p><a href="/meetings/{{.Sqid}}/end">End the meeting</a></p>{{end}}
+{{end}}
diff --git a/internal/meettest/holding_test.go b/internal/meettest/holding_test.go
new file mode 100644
index 0000000..99aa4e9
--- /dev/null
+++ b/internal/meettest/holding_test.go
@@ -0,0 +1,39 @@
+package meettest
+
+import (
+ "net/http"
+ "strings"
+ "testing"
+)
+
+func TestHoldingPageShowsTitleStandingAndFloor(t *testing.T) {
+ x, sqid, _ := memberWithMeeting(t)
+ c := x.browser(t)
+ c.signIn("paul@example.com")
+ rec := c.get("/m/" + sqid + "/join")
+ body := rec.Body.String()
+ for _, want := range []string{"Board meeting", "joining as a member", "no relay yet", "End the meeting"} {
+ if !strings.Contains(body, want) {
+ t.Errorf("holding page lacks %q", want)
+ }
+ }
+}
+
+func TestHoldingPageIs404ForNoStanding(t *testing.T) {
+ x := newHarness(t) // no team: everyone signs in, nobody is a member
+ c := x.browser(t)
+ c.signIn("nobody@example.com")
+ if rec := c.get("/m/2345678abc/join"); rec.Code != http.StatusNotFound {
+ t.Fatalf("no standing: %d", rec.Code)
+ }
+}
+
+func TestHoldingPageIs404ForEndedMeetings(t *testing.T) {
+ x, sqid, _ := memberWithMeeting(t)
+ m := x.browser(t)
+ m.signIn("paul@example.com")
+ m.post("/meetings/"+sqid+"/end", nil)
+ if rec := m.get("/m/" + sqid + "/join"); rec.Code != http.StatusNotFound {
+ t.Fatalf("ended: %d", rec.Code)
+ }
+}