feat(ui): Wild Dragon reskin overlay on the Restreamer UI
Some checks are pending
ci / vet + build (push) Waiting to run
ci / race tests (push) Blocked by required conditions
ci / WebRTC smoke (5-viewer fanout) (push) Blocked by required conditions
ci / WebRTC latency p95 gate (push) Blocked by required conditions

Layers Wild Dragon branding on top of upstream restreamer-ui v1.14.0
without forking the whole repo — keeps upstream UI updates flowing in
when we bump RESTREAMER_UI_REF.

Overlay (deploy/truenas/core/ui-overlay/):
  public/index.html       Wild Dragon title, theme color #0d0e12
  public/manifest.json    PWA name/short_name/colors
  public/favicon.ico      multi-res ICO (16/32/64) generated from
                          a 'WD' monogram in orange #ff6633 on dark
  public/logo192.png      Apple touch icon
  public/logo512.png      PWA install icon
  src/misc/Logo/images/   rs-logo.svg (square mark, used in the
                          Header) and logo.svg (wordmark, used in
                          the Footer) — both Wild-Dragon-themed
  src/misc/Logo/{index,rsLogo}.js
                          link the logos to forge.wilddragon.net
                          instead of datarhei.com

apply-overlay.sh runs in the Docker ui-builder stage just after the
upstream git clone and just before yarn install. Two phases:
  1. rsync the overlay's public/ and src/ on top of the cloned
     upstream tree
  2. Targeted in-place patches for one-line UI strings (header
     title, two welcome captions). Each patch is anchored to a
     unique surrounding context and the script fails loudly if the
     anchor isn't present — so a future upstream rename surfaces
     immediately rather than silently shipping un-rebranded UI.

Image size: ~+50KB (the overlay assets), no measurable build-time
delta. PWA installs and OS bookmarks now show Wild Dragon. The
remaining 'Restreamer'/'datarhei' references in views/Welcome.js,
views/Login.js, views/Settings.js, etc. are deeper-page strings
that aren't worth a one-off overlay; they'll go away when we fork
the UI repo properly for the WebRTC tab milestone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-05-03 13:14:41 +00:00
parent 10f3e20a6a
commit 7621f88fea
11 changed files with 204 additions and 2 deletions

View file

@ -39,9 +39,20 @@ FROM node:21-alpine3.20 AS ui-builder
ARG RESTREAMER_UI_REF=v1.14.0
RUN apk add --no-cache git
WORKDIR /ui
# 1. Pull upstream restreamer-ui at the pinned tag.
RUN git clone --depth=1 --branch ${RESTREAMER_UI_REF} \
https://github.com/datarhei/restreamer-ui.git . \
&& yarn install --frozen-lockfile --network-timeout 600000 \
https://github.com/datarhei/restreamer-ui.git .
# 2. Layer Wild Dragon overlays on top of the upstream tree before the
# build runs. apply-overlay.sh does the rsync + targeted seds; see
# deploy/truenas/core/ui-overlay/apply-overlay.sh for the contract.
COPY deploy/truenas/core/ui-overlay /overlay
RUN OVERLAY=/overlay UI=/ui /overlay/apply-overlay.sh
# 3. Install + build. PUBLIC_URL=./ keeps asset references relative so
# the bundle is portable across mount paths.
RUN yarn install --frozen-lockfile --network-timeout 600000 \
&& PUBLIC_URL="./" GENERATE_SOURCEMAP=false yarn build
# ---- runtime ----

View file

@ -0,0 +1,70 @@
#!/bin/sh
# apply-overlay.sh — Wild Dragon reskin patches applied to a freshly
# cloned datarhei/restreamer-ui tree. Two phases:
#
# 1. File overlay: rsync the contents of $OVERLAY/{public,src} on top
# of the upstream working tree. Whole-file replacements only —
# simple and idempotent.
#
# 2. Targeted in-place sed for one-line UI strings that aren't worth
# a whole-file overlay (the header title, a few welcome strings).
# Each pattern is anchored to a unique surrounding context so a
# future upstream rename doesn't silently rewrite the wrong line.
#
# Caller: the Dockerfile's ui-builder stage. Expects:
# $OVERLAY = /overlay (the COPY destination)
# $UI = /ui (the cloned upstream source root)
#
# Idempotent on a single source tree (rerunning is a no-op).
set -eu
OVERLAY="${OVERLAY:-/overlay}"
UI="${UI:-/ui}"
echo "wilddragon-overlay: layering $OVERLAY -> $UI"
# Phase 1 — file copies. -L follows any future symlinks, -p preserves
# perms, -R recursive. We deliberately avoid --delete: the upstream
# tree must stay intact except for the files we override.
for sub in public src; do
if [ -d "$OVERLAY/$sub" ]; then
cp -RLp "$OVERLAY/$sub/." "$UI/$sub/"
fi
done
# Phase 2 — targeted seds. Each replacement is wrapped in a check so
# the script fails loudly if upstream changed the line we're patching
# (rather than silently no-op'ing and shipping un-rebranded UI).
patch_line() {
file="$1"; needle="$2"; replacement="$3"
if ! grep -qF "$needle" "$file"; then
echo "wilddragon-overlay: ERROR — pattern not found in $file:"
echo " $needle"
exit 1
fi
# Use awk for safe literal substitution (sed's regex would mishandle
# special chars in the replacement).
tmp="$(mktemp)"
awk -v n="$needle" -v r="$replacement" '
index($0, n) { sub(n, r); }
{ print }
' "$file" > "$tmp"
mv "$tmp" "$file"
echo "wilddragon-overlay: patched $(basename "$file")$needle -> $replacement"
}
patch_line "$UI/src/Header.js" \
'<Typography className="headerTitle">Restreamer</Typography>' \
'<Typography className="headerTitle">Wild Dragon</Typography>'
# Welcome view top-of-page card.
patch_line "$UI/src/views/Welcome.js" \
'title="Welcome to Restreamer v2"' \
'title="Welcome to Wild Dragon"'
patch_line "$UI/src/views/Settings.js" \
'title="Welcome to Restreamer v2"' \
'title="Welcome to Wild Dragon"'
echo "wilddragon-overlay: done."

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="favicon.ico" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<meta name="theme-color" content="#0d0e12" />
<meta name="description" content="Wild Dragon — low-latency live video streaming dashboard" />
<link rel="apple-touch-icon" href="logo192.png" />
<link rel="manifest" href="manifest.json" />
<title>Wild Dragon</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,13 @@
{
"short_name": "Wild Dragon",
"name": "Wild Dragon — Live Streaming",
"icons": [
{ "src": "favicon.ico", "sizes": "64x64 32x32 16x16", "type": "image/x-icon" },
{ "src": "logo192.png", "type": "image/png", "sizes": "192x192" },
{ "src": "logo512.png", "type": "image/png", "sizes": "512x512" }
],
"start_url": ".",
"display": "standalone",
"theme_color": "#0d0e12",
"background_color": "#0d0e12"
}

View file

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 70" width="320" height="70">
<!-- Wild Dragon wordmark: small ember+chevron icon followed by the text. -->
<defs>
<linearGradient id="ember-w" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ff8855"/>
<stop offset="1" stop-color="#cc3300"/>
</linearGradient>
</defs>
<!-- icon -->
<g transform="translate(0,4)">
<rect x="2" y="2" width="58" height="58" rx="10" fill="#1a1c22"/>
<path d="M14 48 Q22 30 31 38 Q40 30 48 48 Q40 53 31 47 Q22 53 14 48 Z"
fill="url(#ember-w)" opacity="0.7"/>
<text x="31" y="40" text-anchor="middle"
font-family="'DejaVu Sans','Helvetica',sans-serif"
font-size="26" font-weight="700" fill="#ff6633">WD</text>
</g>
<!-- wordmark -->
<text x="76" y="48"
font-family="'Dosis','Roboto','Helvetica',sans-serif"
font-size="36" font-weight="300" letter-spacing="2" fill="#e7e7ea">
WILD <tspan fill="#ff6633" font-weight="500">DRAGON</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<!-- Wild Dragon mark: dark rounded panel with stylised flame chevron + 'WD' monogram. -->
<defs>
<linearGradient id="ember" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ff6633"/>
<stop offset="1" stop-color="#cc3300"/>
</linearGradient>
</defs>
<rect x="6" y="6" width="188" height="188" rx="32" fill="#0d0e12"/>
<!-- Flame chevron underneath the monogram -->
<path d="M40 150 Q60 110 100 130 Q140 110 160 150 Q140 165 100 152 Q60 165 40 150 Z"
fill="url(#ember)" opacity="0.55"/>
<!-- 'W' -->
<path d="M50 60 L62 130 L78 90 L94 130 L106 60"
stroke="#ff6633" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<!-- 'D' -->
<path d="M118 60 L118 130 L138 130 Q165 130 165 95 Q165 60 138 60 L118 60 Z"
stroke="#ff6633" stroke-width="10" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 975 B

View file

@ -0,0 +1,24 @@
import React from 'react';
import makeStyles from '@mui/styles/makeStyles';
import company_logo from './images/logo.svg';
const useStyles = makeStyles((theme) => ({
Logo: {
height: 27,
},
}));
export default function Logo(props) {
const classes = useStyles();
let link = 'https://forge.wilddragon.net/zgaetano/datarhei-dragonfork-core';
// eslint-disable-next-line no-useless-escape
return (
<a href={link} className={classes.Logo} target="_blank" rel="noopener noreferrer">
<img src={company_logo} alt="Wild Dragon logo" />
</a>
);
}

View file

@ -0,0 +1,24 @@
import React from 'react';
import makeStyles from '@mui/styles/makeStyles';
import company_logo from './images/rs-logo.svg';
const useStyles = makeStyles((theme) => ({
Logo: {
height: 95,
},
}));
export default function Logo(props) {
const classes = useStyles();
let link = 'https://forge.wilddragon.net/zgaetano/datarhei-dragonfork-core';
// eslint-disable-next-line no-useless-escape
return (
<a href={link} className={classes.Logo} target="_blank" rel="noopener noreferrer">
<img src={company_logo} alt="Wild Dragon mark" />
</a>
);
}