feat(service): keep the chooser reachable via /?chooser (refs #451)

With default_landing set to app or admin, "/" redirects straight there,
which made the chooser (and through it the other surface) unreachable
from the "home" link. Add a "?chooser" override: "/" always serves the
chooser when that query is present, regardless of the configured default.

Point the "home" brand links on the player, the admin console, and the
chooser itself at /?chooser, so "home" always lands on the hub instead of
bouncing back through the default redirect. The bare "/" still honours the
default for direct hits and bookmarks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-06-07 15:08:26 +02:00
co-authored by Claude Opus 4.8
parent 38603a6f03
commit d9ef84067e
5 changed files with 36 additions and 12 deletions
@@ -76,6 +76,25 @@ func TestHandleRootRedirects(t *testing.T) {
}
}
// TestHandleRootChooserOverride: "?chooser" forces the chooser even when a
// default redirect is configured, so the hub stays reachable.
func TestHandleRootChooserOverride(t *testing.T) {
for _, landing := range []string{"app", "admin"} {
server := newLandingServer(t, landing)
rec := httptest.NewRecorder()
server.HandleRoot(rec, htmlGet("/?chooser"))
if rec.Code != http.StatusOK {
t.Errorf("landing=%q with ?chooser: status = %d; want 200 (no redirect)", landing, rec.Code)
}
if !strings.Contains(rec.Body.String(), `href="/admin"`) {
t.Errorf("landing=%q with ?chooser: body is not the chooser", landing)
}
}
}
// TestHandleRootJSONIgnoresLanding: API/speaker clients (non-HTML Accept)
// always get the version JSON, never a landing redirect, regardless of
// the configured default.
+14 -9
View File
@@ -117,15 +117,20 @@ func (s *Server) HandleRoot(w http.ResponseWriter, r *http.Request) {
}
// HTML branch: a browser hitting "/". Honour the configured default
// landing surface, otherwise serve the neutral chooser. The admin
// console itself now lives at /admin (served by HandleAdmin).
switch s.defaultLanding() {
case "app":
http.Redirect(w, r, "/app", http.StatusFound)
return
case "admin":
http.Redirect(w, r, "/admin", http.StatusFound)
return
// landing surface, otherwise serve the neutral chooser. A "?chooser"
// query forces the chooser even when a default redirect is set, so the
// hub (and through it the admin console) stays reachable: the "home"
// links on the player and admin point here. The admin console itself
// lives at /admin (served by HandleAdmin).
if !r.URL.Query().Has("chooser") {
switch s.defaultLanding() {
case "app":
http.Redirect(w, r, "/app", http.StatusFound)
return
case "admin":
http.Redirect(w, r, "/admin", http.StatusFound)
return
}
}
w.Header().Set("Content-Type", "text/html")
+1 -1
View File
@@ -9,7 +9,7 @@
</head>
<body>
<header class="topbar">
<a class="brand" href="/" title="AfterTouch home">
<a class="brand" href="/?chooser" title="AfterTouch home">
<img src="/web/img/favicon-braille.svg" alt=""/>
<span class="brand-text">
<span class="brand-name">AfterTouch</span>
+1 -1
View File
@@ -163,7 +163,7 @@
</head>
<body>
<header class="topbar">
<a class="brand" href="/" title="AfterTouch home">
<a class="brand" href="/?chooser" title="AfterTouch home">
<img src="/web/img/favicon-braille.svg" alt=""/>
<span class="brand-text">
<span class="brand-name">AfterTouch</span>
+1 -1
View File
@@ -170,7 +170,7 @@ function App() {
return html`
<div class="app">
<nav class="navbar">
<a class="brand" href="/" title="AfterTouch home">
<a class="brand" href="/?chooser" title="AfterTouch home">
<img src="/app/static/img/logo.svg" alt="AfterTouch" class="nav-logo" />
<div class="brand-text">
<span class="brand-name">AfterTouch</span>