Browse Source

Single room, opt-in nickname, no password; proxy /radio via Apache

Entry flow, permanently (not review-mode toggles):

  - Terms modal deleted, markup and handler.
  - No password anywhere. The field, the /other route and $ROOM_PASSWORD are
    gone. It was never real protection, so nothing was lost -- but the pretense
    is gone with it, which leaves the missing-auth gap visible rather than
    papered over.
  - Nickname is opt-in: a person-icon button sits left of the message box and
    opens the nickname dialog via data-toggle="modal". The dialog is dismissible
    now; unnamed users chat as 'anonymous'. Nothing is demanded on load.

One room, and enforced:

  - Join/leave dropdown and #modal_joinroom removed, along with the now-dead
    room plumbing in chat.js (addRoom/addRoomTab/roomExists/getRoomName and the
    subscribe debug block).
  - The server no longer has subscribe/unsubscribe/getRooms handlers at all.
    That's the point -- hiding the UI alone would still let anyone emit
    'subscribe' by hand and wander into a private room. Every socket joins
    mainRoom on connect and never leaves, so the socket.rooms check in
    'newMessage' can only ever pass for that one room.

Verified against the server: a hand-crafted subscribe is rejected, a message to
an unjoined room reaches nobody, unsubscribe can't strand you out of the Lobby,
and Lobby chat still relays with nicknames.

Apache proxy (production is Apache, not nginx -- the daw vhost already proxies
/ask to another node app "mirrors prod"; the README's nginx snippet was wrong
and is replaced):

    ProxyPass        /radio http://127.0.0.1:3000/radio upgrade=websocket
    ProxyPassReverse /radio http://127.0.0.1:3000/radio

upgrade=websocket needs httpd 2.4.47+ (2.4.63 here), where mod_proxy_http
tunnels upgrades itself -- mod_proxy_wstunnel is not required. Verified with a
websocket-only client through Apache; polling fallback also works. Without it
socket.io would silently degrade to long-polling.

Styling: page background #484c57 (matching header, footer and the main site's
body), the Post button in that same slate rather than Bootstrap blue, and the
Daveo Radio banner gets margin above it -- margin, not padding, since a
background image paints across the padding box.
windhamdavid 1 day ago
parent
commit
df5e494411
7 changed files with 105 additions and 326 deletions
  1. 0 4
      .env.example
  2. 46 27
      README.md
  3. 6 64
      app.js
  4. 21 0
      src/css/main.css
  5. 15 77
      src/index.html
  6. 7 124
      src/js/chat.js
  7. 10 30
      src/js/radio.js

+ 0 - 4
.env.example

@@ -27,10 +27,6 @@ STREAM_MOUNT=/stream
 # Set = socket.io Redis adapter, so multiple processes share rooms/presence.
 # Set = socket.io Redis adapter, so multiple processes share rooms/presence.
 # REDIS_URL=redis://:password@127.0.0.1:6379
 # REDIS_URL=redis://:password@127.0.0.1:6379
 
 
-# NOT authentication -- gates a modal and nothing else. See the /other route
-# in app.js. Real auth still needs doing before this goes on a public domain.
-ROOM_PASSWORD=stillgame
-
 # Emits a test broadcast every 60s.
 # Emits a test broadcast every 60s.
 DEBUG=false
 DEBUG=false
 
 

+ 46 - 27
README.md

@@ -2,9 +2,9 @@
 
 
 Just a litle page on the intrawebs where I can broadcast and chat with friends.
 Just a litle page on the intrawebs where I can broadcast and chat with friends.
 
 
-> **Note:** the password prompt is *not* authentication — it gates a modal and
-> nothing else. The socket connection and the Icecast stream are both reachable
-> without it. See "Auth" below before putting this anywhere public.
+> **Note:** there is no authentication. The page, the chat socket and the Icecast
+> stream are all reachable by anyone who has the URL. See "Auth" below before
+> putting this anywhere public.
 
 
 ---
 ---
 #### Built With:
 #### Built With:
@@ -39,25 +39,34 @@ generated from `src/` and is not in git.
 
 
 #### Serving under davidawindham.com/radio
 #### Serving under davidawindham.com/radio
 
 
-Set `BASE_PATH=/radio` and `TRUST_PROXY=1`, then point nginx at it. `proxy_pass`
-deliberately has **no** trailing path, so the `/radio` prefix is passed through
-intact — the app expects to see it.
-
-```nginx
-location = /radio { return 301 /radio/; }
-
-location /radio/ {
-    proxy_pass http://127.0.0.1:3000;
-    proxy_http_version 1.1;
-    proxy_set_header Upgrade    $http_upgrade;   # websockets
-    proxy_set_header Connection "upgrade";
-    proxy_set_header Host       $host;
-    proxy_set_header X-Real-IP  $remote_addr;
-    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
-    proxy_set_header X-Forwarded-Proto $scheme;
-}
+Set `BASE_PATH=/radio` and `TRUST_PROXY=1`, then proxy to it from Apache — the
+same shape as the existing `/ask` proxy in the daw vhost:
+
+```apache
+# radio: proxy /radio to the node app (mirrors /ask)
+ProxyPreserveHost On
+ProxyPass        /radio http://127.0.0.1:3000/radio upgrade=websocket
+ProxyPassReverse /radio http://127.0.0.1:3000/radio
 ```
 ```
 
 
+Both sides keep the `/radio` prefix on purpose — the app is mounted at it and
+expects to see it, and that keeps its own `/radio` → `/radio/` redirect correct
+through `ProxyPassReverse`.
+
+`upgrade=websocket` is what carries socket.io. It needs **Apache 2.4.47+**, where
+`mod_proxy_http` handles protocol upgrades itself — `mod_proxy_wstunnel` is *not*
+required (on older Apache it would be). Without the upgrade, socket.io still
+works but silently falls back to HTTP long-polling.
+
+ProxyPass is matched before the request is mapped to the filesystem, so
+WordPress and its `.htaccess` never see `/radio`.
+
+> **If `/radio` redirects you to `/online-radio`:** that's a **cached 301**. The
+> WordPress page that used to live at `/radio` moved, and WP issues a permanent
+> old-slug redirect, which browsers cache hard. Once the proxy is in place
+> Apache answers `/radio` before WP ever sees it — but a browser that cached the
+> 301 beforehand will keep redirecting itself. Hard-reload, or clear that entry.
+
 Leave `BASE_PATH` empty to serve at a domain root instead; the client works out
 Leave `BASE_PATH` empty to serve at a domain root instead; the client works out
 where it's mounted at runtime, so the same build covers both.
 where it's mounted at runtime, so the same build covers both.
 
 
@@ -90,7 +99,6 @@ from the docroot alongside `/radio`. Locally there's no Apache in front, so set
 | `/api/status` | now-playing + listeners, proxied from Icecast |
 | `/api/status` | now-playing + listeners, proxied from Icecast |
 | `/api/lastfm` | sidebar data, proxied so the API key stays server-side |
 | `/api/lastfm` | sidebar data, proxied so the API key stays server-side |
 | `/api/broadcast` | POST `{msg}` — sends to every room |
 | `/api/broadcast` | POST `{msg}` — sends to every room |
-| `/other` | the modal's password check (not auth) |
 
 
 All are relative to `BASE_PATH`.
 All are relative to `BASE_PATH`.
 
 
@@ -113,15 +121,26 @@ Optional. Unset `REDIS_URL` and it runs single-process, keeping per-connection
 state in memory. Set it and socket.io uses the Redis adapter so rooms and
 state in memory. Set it and socket.io uses the Redis adapter so rooms and
 presence work across multiple processes.
 presence work across multiple processes.
 
 
+#### Chat
+
+One room. The join/leave UI is gone and the server has no
+`subscribe`/`unsubscribe` handlers, so the Lobby is the only room there is —
+removing the buttons alone wouldn't have stopped anyone emitting `subscribe` by
+hand.
+
+Nicknames are opt-in: the person button beside the message box opens the nickname
+dialog. Nothing is demanded on load; unnamed users chat as `anonymous`.
+
 #### Auth
 #### Auth
 
 
-There isn't any, despite appearances. `ROOM_PASSWORD` is checked client-side via
-the modal's `data-remote` hook; passing it just closes the modal. `/api/broadcast`
-is unauthenticated, the socket handshake is unauthenticated, and the Icecast
-stream can be opened directly.
+There isn't any, and there's no longer anything pretending otherwise — the old
+password prompt was only ever a client-side modal gate, so it was removed rather
+than left to imply protection it never gave.
 
 
-Doing this properly means a server-verified session that covers the socket
-handshake and the stream, not only the page.
+`/api/broadcast` is unauthenticated, the socket handshake is unauthenticated, and
+the Icecast stream can be opened directly. Doing this properly means a
+server-verified session covering the socket handshake and the stream, not just
+the page.
 
 
 ---
 ---
 
 

+ 6 - 64
app.js

@@ -27,8 +27,6 @@ const config = {
   // Number of reverse proxies in front of us, or a preset like 'loopback'.
   // Number of reverse proxies in front of us, or a preset like 'loopback'.
   trustProxy: process.env.TRUST_PROXY ?? '',
   trustProxy: process.env.TRUST_PROXY ?? '',
   debug: process.env.DEBUG === 'true',
   debug: process.env.DEBUG === 'true',
-  // NOT authentication. See the /other route below.
-  roomPassword: process.env.ROOM_PASSWORD ?? 'stillgame',
   // Icecast's own JSON status. The old code polled a hand-customized
   // Icecast's own JSON status. The old code polled a hand-customized
   // status2.xsl over JSONP; Icecast upgrades overwrite those XSL files (it
   // status2.xsl over JSONP; Icecast upgrades overwrite those XSL files (it
   // 404s today, and has since 2021 per the README), so use the stock endpoint
   // 404s today, and has since 2021 per the README), so use the stock endpoint
@@ -52,7 +50,6 @@ function normalizeBasePath(value) {
 
 
 const MAX_MESSAGE_LENGTH = 500;
 const MAX_MESSAGE_LENGTH = 500;
 const MAX_ROOM_LENGTH = 25;
 const MAX_ROOM_LENGTH = 25;
-const MAX_ROOMS_PER_REQUEST = 10;
 
 
 const logger = new EventEmitter();
 const logger = new EventEmitter();
 logger.on('newEvent', (event, data) => {
 logger.on('newEvent', (event, data) => {
@@ -169,16 +166,6 @@ router.post('/api/broadcast', requireAuthentication, (req, res) => {
   res.status(201).send('Message sent to all rooms');
   res.status(201).send('Message sent to all rooms');
 });
 });
 
 
-// The front-end's "password" field posts here via bootstrap-validator's
-// data-remote. This is a speed bump, not a lock: it gates a modal and nothing
-// else. The socket handshake and the Icecast stream remain wide open, so this
-// must not be treated as protection. Real auth is a separate piece of work --
-// it needs to cover the handshake and the stream too, not just this route.
-router.get('/other', (req, res) => {
-  if (req.query.other === config.roomPassword) res.sendStatus(200);
-  else res.status(400).send('WRONG!');
-});
-
 // Local dev only: serve the shared site chrome (/embed/chrome.js + its fonts)
 // Local dev only: serve the shared site chrome (/embed/chrome.js + its fonts)
 // by proxying to the main site.
 // by proxying to the main site.
 //
 //
@@ -338,7 +325,6 @@ io.on('connection', (socket) => {
 
 
   socket.join(config.mainRoom);
   socket.join(config.mainRoom);
   logger.emit('newEvent', 'userJoinsRoom', { socket: socket.id, room: config.mainRoom });
   logger.emit('newEvent', 'userJoinsRoom', { socket: socket.id, room: config.mainRoom });
-  socket.emit('subscriptionConfirmed', { room: config.mainRoom });
   io.to(config.mainRoom).emit('userJoinsRoom', {
   io.to(config.mainRoom).emit('userJoinsRoom', {
     room: config.mainRoom,
     room: config.mainRoom,
     username: socket.data.username,
     username: socket.data.username,
@@ -346,56 +332,12 @@ io.on('connection', (socket) => {
     id: socket.id,
     id: socket.id,
   });
   });
 
 
-  socket.on('subscribe', (data) => {
-    const rooms = Array.isArray(data?.rooms) ? data.rooms.slice(0, MAX_ROOMS_PER_REQUEST) : [];
-    for (const raw of rooms) {
-      const room = cleanRoomName(raw);
-      if (!room) continue;
-
-      socket.join(room);
-      logger.emit('newEvent', 'userJoinsRoom', {
-        socket: socket.id,
-        username: socket.data.username,
-        room,
-      });
-
-      socket.emit('subscriptionConfirmed', { room });
-      io.to(room).emit('userJoinsRoom', {
-        room,
-        username: socket.data.username,
-        msg: '----- Joined the room -----',
-        id: socket.id,
-      });
-    }
-  });
-
-  socket.on('unsubscribe', (data) => {
-    const rooms = Array.isArray(data?.rooms) ? data.rooms.slice(0, MAX_ROOMS_PER_REQUEST) : [];
-    for (const raw of rooms) {
-      const room = cleanRoomName(raw);
-      if (!room || room === config.mainRoom) continue;
-
-      socket.leave(room);
-      logger.emit('newEvent', 'userLeavesRoom', {
-        socket: socket.id,
-        username: socket.data.username,
-        room,
-      });
-
-      socket.emit('unsubscriptionConfirmed', { room });
-      io.to(room).emit('userLeavesRoom', {
-        room,
-        username: socket.data.username,
-        msg: '----- Left the room -----',
-        id: socket.id,
-      });
-    }
-  });
-
-  socket.on('getRooms', () => {
-    socket.emit('roomsReceived', joinedRooms(socket));
-    logger.emit('newEvent', 'userGetsRooms', { socket: socket.id });
-  });
+  // No 'subscribe' / 'unsubscribe' / 'getRooms' handlers: there is one room and
+  // everyone is in it. Dropping them server-side rather than only hiding the UI
+  // is the point -- otherwise anyone could still emit 'subscribe' by hand and
+  // wander off into a private room. Every socket joins config.mainRoom on
+  // connect and never leaves, so the `socket.rooms.has(room)` check in
+  // 'newMessage' below can only ever pass for that one room.
 
 
   socket.on('getUsersInRoom', async (data) => {
   socket.on('getUsersInRoom', async (data) => {
     const room = cleanRoomName(data?.room);
     const room = cleanRoomName(data?.room);

+ 21 - 0
src/css/main.css

@@ -20,6 +20,24 @@ body {
   background: #484c57;
   background: #484c57;
 }
 }
 
 
+/* Post button in the page's own slate instead of Bootstrap's default blue.
+   Targeted by id so it outranks .btn-primary and its :hover/:active variants
+   without having to fight specificity or reach for !important. */
+#b_send_message,
+#b_send_message:focus {
+  background-color: #484c57;
+  border-color: #484c57;
+  color: #fff;
+}
+#b_send_message:hover,
+#b_send_message:active,
+#b_send_message:focus:active,
+#b_send_message.active {
+  background-color: #3a3d46;
+  border-color: #3a3d46;
+  color: #fff;
+}
+
 /* The Daveo Radio banner (daveo-header.svg, a 1280x150 artboard).
 /* The Daveo Radio banner (daveo-header.svg, a 1280x150 artboard).
  *
  *
  * This used to be the background of a bare <header> element. <daw-header> now
  * This used to be the background of a bare <header> element. <daw-header> now
@@ -34,6 +52,9 @@ body {
   min-height: 150px;
   min-height: 150px;
   background: url(../img/daveo-header.svg) no-repeat center center transparent;
   background: url(../img/daveo-header.svg) no-repeat center center transparent;
   background-size: cover;
   background-size: cover;
+  /* margin, not padding: a background image paints across the padding box, so
+     padding-top would just stretch the artwork instead of moving it down. */
+  margin-top: 2rem;
 }
 }
 .outer-container {
 .outer-container {
   width:100%;
   width:100%;

+ 15 - 77
src/index.html

@@ -154,16 +154,10 @@
                
                
                <!-- START Room Tabs -->
                <!-- START Room Tabs -->
                <div class="row-fluid">
                <div class="row-fluid">
+                    <!-- Single room. The join/leave dropdown is gone and the
+                         server no longer accepts subscribe/unsubscribe, so the
+                         Lobby is the only room there is. -->
                     <ul id="rooms_tabs" class="nav nav-tabs">
                     <ul id="rooms_tabs" class="nav nav-tabs">
-                        <li class="dropdown">
-                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" data-original-title="Options" data-placement="bottom"><span class="glyphicon glyphicon-th" aria-hidden="true"></span>&nbsp;<b class="caret"></b></a>
-                            <ul class="dropdown-menu">
-                                <li><a href="#modal_joinroom" data-toggle="modal"><span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span>&nbsp;Join room</a></li>
-                                <li><a id="b_leave_room" href="#"><span class="glyphicon glyphicon-minus-sign" aria-hidden="true"></span>&nbsp;Leave room</a></li>
-                                <li class="divider"></li>
-
-                            </ul>
-                        </li>
                         <li id="Lobby_tab" class="active"><a href="#Lobby" data-toggle="tab" data-original-title="Chat" data-placement="bottom"><span class="glyphicon glyphicon-comment" aria-hidden="true"></span><span class="hidden-tab">Lobby</span></a></li>
                         <li id="Lobby_tab" class="active"><a href="#Lobby" data-toggle="tab" data-original-title="Chat" data-placement="bottom"><span class="glyphicon glyphicon-comment" aria-hidden="true"></span><span class="hidden-tab">Lobby</span></a></li>
                     </ul>
                     </ul>
                </div>
                </div>
@@ -190,13 +184,15 @@
                       <div class="row-fluid">
                       <div class="row-fluid">
                           <form class="form">
                           <form class="form">
                              <div class="input-group">
                              <div class="input-group">
+                                 <span class="input-group-btn">
+                                    <button id="set_nick" type="button" class="btn btn-default" data-toggle="modal" data-target="#modal_setnick" title="Set your nickname"><span class="glyphicon glyphicon-user" aria-hidden="true"></span><span class="sr-only">Set your nickname</span></button>
+                                 </span>
                                 <input id="message_text" class="form-control" type="text" placeholder="What say you?" >
                                 <input id="message_text" class="form-control" type="text" placeholder="What say you?" >
                                  <span class="input-group-btn">
                                  <span class="input-group-btn">
                                     <button id="b_send_message" class="btn btn-primary"><span class="glyphicon glyphicon-bullhorn" aria-hidden="true"></span> Post</button>
                                     <button id="b_send_message" class="btn btn-primary"><span class="glyphicon glyphicon-bullhorn" aria-hidden="true"></span> Post</button>
                                  </span>
                                  </span>
                               </div>
                               </div>
                            </form>
                            </form>
-                           <!--<button id="set_nick" class="btn btn-success" data-toggle="modal" data-target="#modal_setnick"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Set Nick</button>-->
                       </div>
                       </div>
                    </div>
                    </div>
                    <!-- END message -->
                    <!-- END message -->
@@ -208,49 +204,25 @@
                
                
             </div>
             </div>
          
          
-            <!-- START Room Modal -->      
-            <div id="modal_joinroom" class="modal fade">
-               <div class="vertical-alignment-helper">
-                  <div class="modal-dialog modal-sm vertical-align-center">
-                      <div class="modal-content">
-                         <div class="modal-header">
-                             <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-                             <h3>Join Room</h3>
-                         </div>
-                         <div class="modal-body">
-                             <input id="room_name" type="text" class="form-control"  placeholder="Room Name">
-                             <p><small>&nbsp;Room will be created if it doesn't exist</small></p>
-                         </div>
-                         <div class="modal-footer">
-                             <a id="b_join_room" href="#" class="btn btn-success">Join Room</a>
-                         </div>
-                     </div>
-                  </div>
-               </div>
-            </div>
-            <!-- END Room Modal --> 
-            
-            <!-- START Nick Modal --> 
-               <div id="modal_setnick" class="modal fade" data-keyboard="false" data-backdrop="static" >
+            <!-- START Nick Modal -->
+               <!-- Opened from the person button next to the message box, rather
+                    than thrown up on load. Dismissible on purpose now: chatting
+                    as 'anonymous' is allowed, setting a nickname is opt-in. -->
+               <div id="modal_setnick" class="modal fade">
                   <div class="vertical-alignment-helper">
                   <div class="vertical-alignment-helper">
                      <div class="modal-dialog modal-sm">
                      <div class="modal-dialog modal-sm">
                         <div class="modal-content">
                         <div class="modal-content">
                            <form id="nick" role="form">
                            <form id="nick" role="form">
+                              <div class="modal-header">
+                                 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+                                 <h4 class="modal-title">Set your nickname</h4>
+                              </div>
                               <div class="modal-body">
                               <div class="modal-body">
                                  <div class="form-group has-feedback">
                                  <div class="form-group has-feedback">
                                     <label class="control-label" for="nickname">Nickname:</label>
                                     <label class="control-label" for="nickname">Nickname:</label>
                                     <div class="input-group">
                                     <div class="input-group">
                                        <span class="input-group-addon">@</span>
                                        <span class="input-group-addon">@</span>
                                        <input id="nickname" type="text" class="form-control" pattern="^[a-zA-Z]+$" minlength="3" maxlength="25" placeholder="letters only (min 3)" required>
                                        <input id="nickname" type="text" class="form-control" pattern="^[a-zA-Z]+$" minlength="3" maxlength="25" placeholder="letters only (min 3)" required>
-                                       <span class="glyphicon form-control-feedback" aria-hidden="true"></span>               
-                                    </div>
-                                    <div class="help-block with-errors"></div>
-                                 </div>
-                                 <div class="form-group has-feedback">
-                                    <label class="control-label" for="password">Password:</label>
-                                    <div class="input-group">                  
-                                       <span class="input-group-addon"><span class="glyphicon glyphicon-cog" aria-hidden="true"></span></span>
-                                       <input id="other" name="other" type="text" class="form-control" data-remote="other" placeholder="" required>
                                        <span class="glyphicon form-control-feedback" aria-hidden="true"></span>
                                        <span class="glyphicon form-control-feedback" aria-hidden="true"></span>
                                     </div>
                                     </div>
                                     <div class="help-block with-errors"></div>
                                     <div class="help-block with-errors"></div>
@@ -266,40 +238,6 @@
                </div>
                </div>
             <!-- END Nick Modal -->
             <!-- END Nick Modal -->
             
             
-            <!-- START Auth Modal -->
-            <div class="modal fade" id="auth-modal" data-keyboard="false" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="support" aria-hidden="true">
-              <div class="modal-dialog" role="document">
-                <div class="modal-content">
-                  <div class="modal-header">
-                    <h4 class="modal-title"> You kids get off my &nbsp;<span class="glyphicon glyphicon-cloud" aria-hidden="true"></span></h4>
-                  </div>
-                  <div class="modal-body">
-                     <h5>Just some basic rules here:</h5>
-                     <ol>
-                        <li>This site is for demonstration purposes.</li>
-                        <li>There is no reason to be here unless I am.</li>
-                        <li>If you don't know me, you shouldn't be here.</li>
-                        <li>Do Not Share this URL with Anyone.</li>
-                     </ol>                     
-                  </div>
-                  <div class="modal-footer">
-                     <form role="form" id="auth" class="form">
-                        <div class="form-group">
-                           <div class="checkbox">
-                              <label><input type="checkbox" id="terms" data-error="really? So you don't agree?" required>I have read and agree</label>
-                              <div class="help-block with-errors"></div>
-                           </div>
-                        </div>
-                        <div class="form-group">
-                           <button type="submit" class="btn btn-success">Understood</button>
-                        </div>
-                     </form>
-                  </div>
-                </div>
-              </div>
-            </div>
-            <!-- END Auth Modal -->
-            
          </section>
          </section>
       </div>
       </div>
       <!-- End Connect -->
       <!-- End Connect -->

+ 7 - 124
src/js/chat.js

@@ -1,11 +1,9 @@
 (function(){
 (function(){
 
 
-    var debug = false;
-
     // ***************************************************************************
     // ***************************************************************************
     // Socket.io events
     // Socket.io events
     // ***************************************************************************
     // ***************************************************************************
-    
+
     var socket = window.RADIO.socket;
     var socket = window.RADIO.socket;
 
 
     // Connection established
     // Connection established
@@ -14,20 +12,6 @@
 
 
         // Get users connected to mainroom
         // Get users connected to mainroom
         socket.emit('getUsersInRoom', {'room':'Lobby'});
         socket.emit('getUsersInRoom', {'room':'Lobby'});
-
-        if (debug) {
-            // Subscription to rooms
-            socket.emit('subscribe', {'username':'sergio', 'rooms':['sampleroom']});
-
-            // Send sample message to room
-            socket.emit('newMessage', {'room':'sampleroom', 'msg':'Hellooooooo!'});
-
-            // Auto-disconnect after 10 minutes
-            setInterval(function() {
-                socket.emit('unsubscribe', {'rooms':['sampleroom']});
-                socket.disconnect();
-            }, 600000);
-        }
     });
     });
 
 
     // Disconnected from server
     // Disconnected from server
@@ -42,26 +26,9 @@
         addBotMessage(info);
         addBotMessage(info);
     });
     });
 
 
-    // Subscription to room confirmed
-    socket.on('subscriptionConfirmed', function(data) {
-        // Create room space in interface
-        if (!roomExists(data.room)) {
-            addRoomTab(data.room);
-            addRoom(data.room);
-        }
-
-        // Close modal if opened
-        $('#modal_joinroom').modal('hide');
-    });
-
-    // Unsubscription to room confirmed
-    socket.on('unsubscriptionConfirmed', function(data) {
-        // Remove room space in interface
-        if (roomExists(data.room)) {
-            removeRoomTab(data.room);
-            removeRoom(data.room);
-        }
-    });
+    // subscriptionConfirmed / unsubscriptionConfirmed are gone with rooms. The
+    // Lobby tab is static markup, so there is no room UI left to build or tear
+    // down at runtime.
 
 
     // User joins room
     // User joins room
     socket.on('userJoinsRoom', function(data) {
     socket.on('userJoinsRoom', function(data) {
@@ -140,39 +107,6 @@
         }
         }
     }
     }
 
 
-    // Add room tab
-    var addRoomTab = function(room) {
-        getTemplate('js/templates/room_tab.handlebars', function(template) {
-            $('#rooms_tabs').append(template({'room':room}));
-        });
-    };
-
-    // Remove room tab
-    var removeRoomTab = function(room) {
-        var tab_id = "#"+room+"_tab";
-        $(tab_id).remove();
-    };
-
-    // Add room
-    var addRoom = function(room) {
-        getTemplate('js/templates/room.handlebars', function(template) {
-            $('#rooms').append(template({'room':room}));
-        
-            // Toogle to created room
-            var newroomtab = '[href="#'+room+'"]';
-            $(newroomtab).click();
-
-            // Get users connected to room
-            socket.emit('getUsersInRoom', {'room':room});
-        });
-    };
-    
-    // Remove room
-    var removeRoom = function(room) {
-        var room_id = "#"+room;
-        $(room_id).remove();
-    };
-
     // Add message to room
     // Add message to room
     var addMessage = function(msg) {
     var addMessage = function(msg) {
         getTemplate('js/templates/message.handlebars', function(template) {
         getTemplate('js/templates/message.handlebars', function(template) {
@@ -207,16 +141,6 @@
         $(user_badge).remove();
         $(user_badge).remove();
     };
     };
 
 
-    // Check if room exists
-    var roomExists = function(room) {
-        var room_selector = '#'+room;
-        if ($(room_selector).length) {
-            return true;
-        } else {
-            return false;
-        }
-    };
-
     // Get current room
     // Get current room
     var getCurrentRoom = function() {
     var getCurrentRoom = function() {
         return $('li[id$="_tab"][class="active"]').text();
         return $('li[id$="_tab"][class="active"]').text();
@@ -229,13 +153,6 @@
         return text;
         return text;
     };
     };
 
 
-    // Get room name from input field
-    var getRoomName = function() {
-        var name = $('#room_name').val().trim();
-        $('#room_name').val("");
-        return name;
-    };
-
     // Get nickname from input field
     // Get nickname from input field
     var getNickname = function() {
     var getNickname = function() {
         var nickname = $('#nickname').val();
         var nickname = $('#nickname').val();
@@ -261,43 +178,9 @@
         }
         }
     });
     });
 
 
-    // Join new room
-    $('#b_join_room').click(function(eventObject) {
-        var roomName = getRoomName();
-
-        if (roomName) {
-            eventObject.preventDefault();
-            socket.emit('subscribe', {'rooms':[roomName]}); 
-
-        // Added error class if empty room name
-        } else {
-            $('#room_name').addClass('error');
-        }
-    });
-
-    // Leave current room
-    $('#b_leave_room').click(function(eventObject) {
-        eventObject.preventDefault();
-        var currentRoom = getCurrentRoom();
-        if (currentRoom != 'Lobby') {
-            socket.emit('unsubscribe', {'rooms':[getCurrentRoom()]}); 
-
-            // Toogle to MainRoom
-            $('[href="#Lobby"]').click();
-        } else {
-            console.log('Cannot leave Lobby, sorry');
-        }
-    });
-
-    // Remove error style to hide modal
-    $('#modal_joinroom').on('hidden.bs.modal', function (e) {
-        if ($('#room_name').hasClass('error')) {
-            $('#room_name').removeClass('error');
-        }
-    });
-
-    // Set nickname
-
+    // Joining and leaving rooms is gone -- there is one room now, and the
+    // server no longer accepts subscribe/unsubscribe, so it isn't just the UI
+    // that's hidden.
 
 
 })();
 })();
 
 

+ 10 - 30
src/js/radio.js

@@ -1,22 +1,12 @@
 /*global amplitude_config:true */
 /*global amplitude_config:true */
 
 
-// ===========================================================================
-// TEMPORARY (2026-07-16) — REVIEW MODE. Set to false to restore normal behaviour.
+// TEMPORARY (2026-07-16) — suppresses the "Off the Air" modal, which pops up
+// whenever the stream has no source (i.e. right now) and covers the page.
 //
 //
-// Suppresses every modal that covers the page on load, so the layout and the
-// site chrome can be looked at without clicking through:
-//   - #auth-modal      terms / "I have read and agree"
-//   - #modal_setnick   nickname + password (chained from the terms modal)
-//   - #connection-error "Off the Air" (shows whenever the stream has no source,
-//                       which is right now — it would cover the page otherwise)
-//
-// Only the modal *display* is suppressed. All the markup and handlers are
-// untouched, so flipping this back restores the entry flow exactly. The off-air
-// artwork still swaps in, so the player still reads as off air.
-//
-// Nothing security-relevant is lost here: the password modal never was real
-// protection (see the /other route in app.js).
-// ===========================================================================
+// This is all that's left of review mode: the terms and nickname/password
+// modals are gone for good now, not just hidden. The off-air artwork still
+// swaps in either way, so the player reads as off air regardless — this only
+// stops the dialog. Set to false to get the dialog back.
 var REVIEW_MODE = true;
 var REVIEW_MODE = true;
 
 
 
 
@@ -224,20 +214,10 @@ interval = setInterval(radioTitle,20000); // every 20 seconds
 /**** Page Features ****/
 /**** Page Features ****/
 
 
 $(document).ready(function() {
 $(document).ready(function() {
-  // See REVIEW_MODE at the top of this file. Skipping the terms modal also skips
-  // the nickname/password modal it chains into, so everyone stays 'anonymous'
-  // in chat while review mode is on.
-  if (!REVIEW_MODE) {
-    $('#auth-modal').modal('show');
-  }
-  $('#auth').validator().on('submit', function (e) {
-    if (e.isDefaultPrevented()) {
-    } else {
-      e.preventDefault();
-      $('#auth-modal').modal('hide');
-      $('#modal_setnick').modal('show');
-    }
-  });
+  // Nothing is thrown up on load any more. The terms modal is gone, and the
+  // nickname modal opens from the person button beside the message box
+  // (data-toggle="modal" wires that up, no JS needed). Chatting as 'anonymous'
+  // is fine; naming yourself is opt-in.
   $('#nick').validator();
   $('#nick').validator();
   var socket = window.RADIO.socket;
   var socket = window.RADIO.socket;
   var getNickname = function() {
   var getNickname = function() {