Browse Source

Use the shared site chrome; fix a CSS concat bug that ate rules

Renders the site header/footer via <daw-header>/<daw-footer> + /embed/chrome.js
-- the same mechanism /rtc already uses.

This looked like it needed a Bootstrap 5 migration. daw's chrome markup is BS5,
this page is BS3, and both claim .container/.row/.col-*/.navbar/.btn, so loading
them together collides; daw's stylesheet is also a subset with no .modal and no
.nav-tabs, which is exactly what this UI is built from (76 modal refs, 7
tab-panes, 54 glyphicons). None of that was needed: chrome.js renders into a
shadow root with :host{all:initial}, so it's sealed off from Bootstrap 3.
Nothing is copied here -- the chrome stays maintained in the main site and
can't drift.

The script tag is root-relative because Apache serves /embed from the WP
docroot alongside /radio in production; nginx only routes /radio/ to node.
Locally there's no Apache in front, so app.js proxies /embed to $DAW_ORIGIN.
Dev-only -- unset in production.

BUILD FIX: bundles were concatenated with '\n;\n'. That is right for JS (stops a
file ending in an expression from swallowing the next file's leading paren) and
invalid at CSS top level -- the parser's error recovery folds the stray ';' into
the next rule's selector, silently dropping that rule. It had been eating the
first rule of main.css since the build was written, which was the body
background. No warning, no console error. Separator is now per-language.

CHROME SIZING: shadow DOM does not isolate rem -- it resolves against the
document root regardless, so :host{all:initial} can't stop it. Bootstrap 3's
html{font-size:10px} was rendering the whole chrome at 62.5% (34px bar, 14.5px
brand vs 52px/23.2px on the rest of the site). main.css now restores a 16px
root; safe because nothing here uses rem units (0 in Bootstrap 3.4.1, 0 in
main.css -- BS3 sizes in px). Radio now matches /rtc exactly: root 16px, bar
52px, pad 12px, brand 23.2px.

Page background is now #484c57, matching the header, the footer, and the main
site's own body.

The Daveo Radio graphic is back as .radio-banner. It was the background of the
old <header> band, which <daw-header> replaced; it now sits below the chrome
rather than standing in for it. Note it's black artwork (daveo-header.svg
declares no fills) so it's low-contrast on the new dark background -- left as a
design call.

TEMPORARY: REVIEW_MODE=true in src/js/radio.js suppresses the terms, nickname/
password and off-air modals so the page can be reviewed without clicking
through. Set false to restore; markup and handlers untouched.

Verified in a real browser: both elements upgrade, brand right-aligned,
offcanvas opens on click, magic-line underline tracks hover (left 0->53->100->
155px), footer renders 21 links + sprite icons, radio's own player/chat/sidebar
intact, no console errors.
windhamdavid 1 day ago
parent
commit
4d4d4e7e51
7 changed files with 140 additions and 49 deletions
  1. 6 0
      .env.example
  2. 20 0
      README.md
  3. 30 0
      app.js
  4. 10 4
      build.mjs
  5. 34 33
      src/css/main.css
  6. 11 9
      src/index.html
  7. 29 3
      src/js/radio.js

+ 6 - 0
.env.example

@@ -38,3 +38,9 @@ DEBUG=false
 # Unset => the sidebar lists stay empty.
 LASTFM_API_KEY=
 LASTFM_USER=windhamdavid
+
+# Local dev only. index.html loads the shared site chrome from /embed/chrome.js,
+# which Apache serves from the WP docroot in production. Locally there's no
+# Apache in front of node, so point this at the main site to proxy it.
+# Leave UNSET in production.
+# DAW_ORIGIN=http://daw.stu

+ 20 - 0
README.md

@@ -61,6 +61,26 @@ location /radio/ {
 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.
 
+#### Site chrome
+
+The header and footer come from the main site's shared web components — the same
+ones `/rtc` uses:
+
+```html
+<daw-header></daw-header>
+<daw-footer></daw-footer>
+<script src="/embed/chrome.js"></script>
+```
+
+`chrome.js` renders into a shadow root, so the site's styles stay sealed off from
+this page's Bootstrap 3 and the two can't collide. Nothing is copied into this
+repo — the chrome is maintained in the main site and can't drift out of sync here.
+
+That script tag is root-relative on purpose: in production Apache serves `/embed`
+from the docroot alongside `/radio`. Locally there's no Apache in front, so set
+`DAW_ORIGIN=http://daw.stu` and the app proxies `/embed` to it. **Leave
+`DAW_ORIGIN` unset in production.**
+
 #### Routes
 
 | Route | Purpose |

+ 30 - 0
app.js

@@ -40,6 +40,8 @@ const config = {
   // Unset => the sidebar lists just stay empty.
   lastfmKey: process.env.LASTFM_API_KEY ?? '',
   lastfmUser: process.env.LASTFM_USER ?? 'windhamdavid',
+  // Local dev only. See the /embed proxy below.
+  dawOrigin: process.env.DAW_ORIGIN ?? '',
 };
 
 function normalizeBasePath(value) {
@@ -177,6 +179,34 @@ router.get('/other', (req, res) => {
   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.
+//
+// index.html loads /embed/chrome.js root-relative. In production that path is
+// served by Apache from the WordPress docroot, alongside /radio -- nginx only
+// routes /radio/ here, so this never runs there. Locally there's no Apache in
+// front, so without this the chrome would 404 and the page would render bare.
+// Unset DAW_ORIGIN => not mounted at all.
+if (config.dawOrigin) {
+  app.use('/embed', async (req, res) => {
+    try {
+      const upstream = await fetch(new URL(`/embed${req.url}`, config.dawOrigin), {
+        signal: AbortSignal.timeout(5000),
+      });
+      if (!upstream.ok) {
+        res.sendStatus(upstream.status);
+        return;
+      }
+      const type = upstream.headers.get('content-type');
+      if (type) res.type(type);
+      res.send(Buffer.from(await upstream.arrayBuffer()));
+    } catch {
+      res.sendStatus(502);
+    }
+  });
+  logger.emit('newEvent', 'embedProxyEnabled', { origin: config.dawOrigin });
+}
+
 // Canonicalize /radio -> /radio/ before the router sees it. Without the
 // trailing slash the browser resolves the page's relative asset URLs against
 // the parent directory, so every script and stylesheet 404s.

+ 10 - 4
build.mjs

@@ -70,17 +70,23 @@ const DEFINE = {
   __STREAM_MOUNT__: JSON.stringify(STREAM_MOUNT),
 };
 
-async function concat(files) {
+// `separator` differs by language and it matters. JS files are joined with a
+// semicolon so a file ending in an expression can't swallow the next file's
+// leading paren (the classic IIFE concat hazard). CSS must NOT get one: a
+// stray top-level ';' is a parse error, and the recovery rule makes the parser
+// consume it as part of the *next* rule's selector -- silently dropping that
+// rule. It ate main.css's first rule until this was fixed.
+async function concat(files, separator) {
   const parts = [];
   for (const file of files) {
     if (!existsSync(file)) throw new Error(`missing build input: ${path.relative(__dirname, file)}`);
     parts.push(await readFile(file, 'utf8'));
   }
-  return parts.join('\n;\n');
+  return parts.join(separator);
 }
 
 async function bundleJs(files, outFile, { define } = {}) {
-  const source = await concat(files);
+  const source = await concat(files, '\n;\n');
   // These are old-style global scripts, not modules -- minify only, no bundling
   // or module resolution, or they'd lose their globals.
   const result = await transform(source, { loader: 'js', minify: true, define });
@@ -88,7 +94,7 @@ async function bundleJs(files, outFile, { define } = {}) {
 }
 
 async function bundleCss(files, outFile) {
-  const source = await concat(files);
+  const source = await concat(files, '\n');
   const result = await transform(source, { loader: 'css', minify: true });
   await writeFile(path.join(OUT, outFile), result.code);
 }

+ 34 - 33
src/css/main.css

@@ -1,29 +1,39 @@
 /*! Main CSS file */
 
+/* Undo Bootstrap 3's `html{font-size:10px}`.
+ *
+ * <daw-header>/<daw-footer> size themselves in rem, and rem always resolves
+ * against the document root -- shadow DOM doesn't isolate it, so `all:initial`
+ * on :host doesn't help. Bootstrap's 10px root was silently rendering the whole
+ * chrome at 62.5%: a 34px bar with a 14.5px brand, against 52px/23.2px on the
+ * rest of the site.
+ *
+ * Safe to override: nothing here uses rem units (Bootstrap 3 sizes in px and
+ * sets body{font-size:14px} explicitly), so this only affects the chrome.
+ */
+html {
+  font-size: 16px;
+}
+
+/* #484c57 matches the header and footer, and the main site's own body. */
 body {
-  background-repeat:no-repeat;
-  background-size:cover;
-  background-position: center center;
-  background:url(../img/daveo-background.svg) transparent;
-  background:url(../img/daveo-background.svg),radial-gradient(yellow,#f06d06);
-
-/*
-  background: -webkit-gradient(radial, center center, 0, center center, 460, from(transparent), to(#333));
-  background: -webkit-radial-gradient(circle, transparent, #333);
-  background: -moz-radial-gradient(circle, transparent, #333);
-  background: -ms-radial-gradient(circle, transparent, #333);
-*/
-}
-
-header {
-  width:100%;
-  min-height:150px;
-  background:url(../img/daveo-header.svg) transparent;
-  background-repeat:no-repeat;
-  background-size:cover;
-  background-position:center;
-  padding: 50px 0 0;
-  margin: 0;
+  background: #484c57;
+}
+
+/* The Daveo Radio banner (daveo-header.svg, a 1280x150 artboard).
+ *
+ * This used to be the background of a bare <header> element. <daw-header> now
+ * provides the site chrome, so the graphic gets its own element and sits below
+ * it. Same sizing as before, minus the old 50px top padding, which existed to
+ * clear the band's own artwork.
+ *
+ * The matching daveo-footer.svg band is still retired -- <daw-footer> occupies
+ * that end of the page now. It's in src/img/ if you want it back too. */
+.radio-banner {
+  width: 100%;
+  min-height: 150px;
+  background: url(../img/daveo-header.svg) no-repeat center center transparent;
+  background-size: cover;
 }
 .outer-container {
   width:100%;
@@ -573,16 +583,7 @@ ul .badge {
   height:410px;
 }
 
-footer {
-  width:100%;
-  min-height:100px;
-  background:url(../img/daveo-footer.svg) transparent;
-  background-repeat:no-repeat;
-  background-size:cover;
-  background-position:center;
-  padding: 50px 0 0;
-  margin: 0 0 20px;
-}
+/* footer{} band removed with the header band above -- see note there. */
 
 
 

+ 11 - 9
src/index.html

@@ -9,13 +9,14 @@
 </head>
 <body>
 
-<header>
-	<div class="container">
-		<div class="intro-text">
+<!-- Shared site chrome, same as /rtc uses. Defined by /embed/chrome.js, which
+     renders into a shadow root -- so its Bootstrap-5-era styles are sealed off
+     from this page's Bootstrap 3 and the two can't collide. -->
+<daw-header></daw-header>
 
-		</div>
-	</div>
-</header>
+<!-- Daveo Radio banner. Was the background of the old <header> band; it now sits
+     under the site chrome rather than standing in for it. -->
+<div class="radio-banner"></div>
 
 <div class="outer-container">
 <div class="container">
@@ -429,14 +430,15 @@
 
 
 
-<footer class="site-footer">
-   
-</footer>
+<daw-footer></daw-footer>
 
 <script src="js/vendor.min.js"></script>
 <!-- Relative on purpose: resolves under /radio/ as well as at the root. -->
 <script src="socket.io/socket.io.js"></script>
 <script src="js/radio.min.js"></script>
+<!-- Root-relative, not relative: in production Apache serves this from the WP
+     docroot alongside /radio. Locally, app.js proxies /embed to $DAW_ORIGIN. -->
+<script src="/embed/chrome.js"></script>
 <script>var _paq = _paq || [];_paq.push(['trackPageView']);_paq.push(['enableLinkTracking']);(function(){var u="//davidawindham.com/wik/";_paq.push(['setTrackerUrl', u+'piwik.php']);_paq.push(['setSiteId', 1]);var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);})();</script><noscript><p><img src="//davidawindham.com/wik/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
 </body>
 </html>

+ 29 - 3
src/js/radio.js

@@ -1,5 +1,24 @@
 /*global amplitude_config:true */
 
+// ===========================================================================
+// TEMPORARY (2026-07-16) — REVIEW MODE. Set to false to restore normal behaviour.
+//
+// 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).
+// ===========================================================================
+var REVIEW_MODE = true;
+
 
 /* Radio Funtions */
 
@@ -155,7 +174,9 @@ function get_radio_eq_none() {return 'img/1.png';}
 var interval = null;
 
 function showOffAir() {
-  $('#connection-error').modal('show');
+  if (!REVIEW_MODE) {
+    $('#connection-error').modal('show');
+  }
   $('#error-reconnecting').hide();
   $('#error-reconnecting-again').hide();
   $('#connection-error-reconnecting').attr('data-transitiongoal', 0).progressbar();
@@ -203,7 +224,12 @@ interval = setInterval(radioTitle,20000); // every 20 seconds
 /**** Page Features ****/
 
 $(document).ready(function() {
-  $('#auth-modal').modal('show');
+  // 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 {
@@ -211,7 +237,7 @@ $(document).ready(function() {
       $('#auth-modal').modal('hide');
       $('#modal_setnick').modal('show');
     }
-  }); 
+  });
   $('#nick').validator();
   var socket = window.RADIO.socket;
   var getNickname = function() {