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.
 # 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.
 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.
 
-> **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:
@@ -39,25 +39,34 @@ generated from `src/` and is not in git.
 
 #### 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
 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/lastfm` | sidebar data, proxied so the API key stays server-side |
 | `/api/broadcast` | POST `{msg}` — sends to every room |
-| `/other` | the modal's password check (not auth) |
 
 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
 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
 
-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'.
   trustProxy: process.env.TRUST_PROXY ?? '',
   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
   // 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
@@ -52,7 +50,6 @@ function normalizeBasePath(value) {
 
 const MAX_MESSAGE_LENGTH = 500;
 const MAX_ROOM_LENGTH = 25;
-const MAX_ROOMS_PER_REQUEST = 10;
 
 const logger = new EventEmitter();
 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');
 });
 
-// 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)
 // by proxying to the main site.
 //
@@ -338,7 +325,6 @@ io.on('connection', (socket) => {
 
   socket.join(config.mainRoom);
   logger.emit('newEvent', 'userJoinsRoom', { socket: socket.id, room: config.mainRoom });
-  socket.emit('subscriptionConfirmed', { room: config.mainRoom });
   io.to(config.mainRoom).emit('userJoinsRoom', {
     room: config.mainRoom,
     username: socket.data.username,
@@ -346,56 +332,12 @@ io.on('connection', (socket) => {
     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) => {
     const room = cleanRoomName(data?.room);

+ 21 - 0
src/css/main.css

@@ -20,6 +20,24 @@ body {
   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).
  *
  * This used to be the background of a bare <header> element. <daw-header> now
@@ -34,6 +52,9 @@ body {
   min-height: 150px;
   background: url(../img/daveo-header.svg) no-repeat center center transparent;
   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 {
   width:100%;

+ 15 - 77
src/index.html

@@ -154,16 +154,10 @@
                
                <!-- START Room Tabs -->
                <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">
-                        <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>
                     </ul>
                </div>
@@ -190,13 +184,15 @@
                       <div class="row-fluid">
                           <form class="form">
                              <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?" >
                                  <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>
                                  </span>
                               </div>
                            </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>
                    <!-- END message -->
@@ -208,49 +204,25 @@
                
             </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="modal-dialog modal-sm">
                         <div class="modal-content">
                            <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="form-group has-feedback">
                                     <label class="control-label" for="nickname">Nickname:</label>
                                     <div class="input-group">
                                        <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>
-                                       <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>
                                     </div>
                                     <div class="help-block with-errors"></div>
@@ -266,40 +238,6 @@
                </div>
             <!-- 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>
       </div>
       <!-- End Connect -->

+ 7 - 124
src/js/chat.js

@@ -1,11 +1,9 @@
 (function(){
 
-    var debug = false;
-
     // ***************************************************************************
     // Socket.io events
     // ***************************************************************************
-    
+
     var socket = window.RADIO.socket;
 
     // Connection established
@@ -14,20 +12,6 @@
 
         // Get users connected to mainroom
         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
@@ -42,26 +26,9 @@
         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
     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
     var addMessage = function(msg) {
         getTemplate('js/templates/message.handlebars', function(template) {
@@ -207,16 +141,6 @@
         $(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
     var getCurrentRoom = function() {
         return $('li[id$="_tab"][class="active"]').text();
@@ -229,13 +153,6 @@
         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
     var getNickname = function() {
         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 */
 
-// ===========================================================================
-// 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;
 
 
@@ -224,20 +214,10 @@ interval = setInterval(radioTitle,20000); // every 20 seconds
 /**** Page Features ****/
 
 $(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();
   var socket = window.RADIO.socket;
   var getNickname = function() {