diff --git a/.augment/README.md b/.augment/README.md deleted file mode 100644 index ab2956a..0000000 --- a/.augment/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# .augment - -Project AI rules live in the repo root: **CLAUDE.md**. Use it for workflow, branch flow, and critical patterns. diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 231d09c..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(tasklist:*)", - "Bash(findstr:*)", - "Bash(ipconfig:*)", - "Bash(npm start)", - "mcp__MCP_DOCKER__browser_navigate", - "mcp__MCP_DOCKER__browser_take_screenshot", - "Bash(curl:*)", - "Bash(1 -H \"Icy-MetaData: 1\")", - "Bash(start \"\" \"c:\\\\Users\\\\Username\\\\Documents\\\\github\\\\LANStreamer\\\\Start LANStreamer Server.bat\")", - "Bash(timeout /t 5 /nobreak)", - "Bash(node:*)", - "Bash(cmd /c \"cd /d C:\\\\Users\\\\Username\\\\Documents\\\\github\\\\LANStreamer && dir package.json && dir node_modules >nul 2>&1 && echo node_modules exists || echo node_modules NOT found\")", - "mcp__MCP_DOCKER__browser_network_requests", - "Bash(cmd /c \"cd /d C:\\\\Users\\\\Username\\\\Documents\\\\github\\\\LANStreamer && Start~1~LAN~1.Server.bat 2>&1\")", - "Bash(git branch:*)", - "Bash(cmd /c \"node --version && echo Node.js OK\")", - "Bash(cmd /c:*)", - "Bash(git fetch:*)", - "Bash(netstat:*)", - "Bash(taskkill:*)", - "Bash(powershell -Command \"Stop-Process -Id 57624 -Force\")", - "mcp__MCP_DOCKER__fetch", - "Bash(cmd.exe /c \"tasklist /FI \"\"IMAGENAME eq icecast.exe\"\"\")", - "Bash(cmd.exe /c \"netstat -ano | findstr :8000\")", - "Bash(cmd.exe /c \"ffmpeg -version\")", - "Bash(git checkout:*)", - "Bash(git log:*)", - "Bash(for version in v1.2.0 v1.2.2 v1.2.3)", - "Bash(do echo \"== $version ==\")", - "Bash(done)", - "Bash(dir:*)", - "Bash(powershell \"Select-Object -Last 5\")", - "Bash(npm update:*)", - "Bash(if not exist \"C:\\\\Users\\\\Username\\\\.claude\\\\skills\" mkdir \"C:\\\\Users\\\\Username\\\\.claude\\\\skills\")", - "Bash(powershell:*)", - "Bash(more:*)", - "Bash(type:*)", - "Bash(del \"C:\\\\Users\\\\Username\\\\Documents\\\\github\\\\LANStreamer\\\\docs\\\\ICECAST_CONFIG_ARCHITECTURE.md\")", - "Bash(npm test:*)", - "Bash(gh run list:*)", - "Bash(gh run view:*)", - "Bash(xargs:*)", - "Bash(cmd.exe /c \"netstat -ano | findstr :3001\")", - "Bash(tail:*)", - "Bash(timeout 5 ffmpeg:*)", - "Bash(timeout 3 ffmpeg:*)", - "Bash(git stash:*)", - "Bash(git pull:*)", - "Bash(git merge:*)", - "Bash(gh pr create:*)", - "Bash(gh pr view:*)", - "Bash(gh pr edit:*)", - "Bash(git rebase:*)" - ] - } -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f5766e..7338566 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,5 +29,8 @@ jobs: - name: Install dependencies run: npm ci + - name: Integrity check (design tokens + UI rules) + run: node scripts/check-design-tokens.mjs + - name: Run tests run: npm test diff --git a/.github/workflows/enforce-prs.yml b/.github/workflows/enforce-prs.yml new file mode 100644 index 0000000..c195299 --- /dev/null +++ b/.github/workflows/enforce-prs.yml @@ -0,0 +1,57 @@ +name: enforce-prs + +# Free-tier branch protection (adopted from the devops repo, 2026-10-28): +# every commit that lands on main must (a) belong to a merged PR and (b) +# have a green CI run for that exact commit. Direct pushes and red-CI +# merges are auto-reverted (GITHUB_TOKEN pushes do not re-trigger +# workflows, so the revert itself doesn't loop). dev stays open for the +# documented local-merge workflow; only main (the release branch) is +# protected. Replace with native branch protection if the repo upgrades +# to GitHub Pro or goes public. + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +jobs: + verify-pr: + if: > + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.event == 'push' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - name: Require green CI and PR association for this commit + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + SHA: ${{ github.event.workflow_run.head_sha }} + CONCLUSION: ${{ github.event.workflow_run.conclusion }} + run: | + if [ "$CONCLUSION" != "success" ]; then + echo "::error::CI concluded '$CONCLUSION' for $SHA on main. This commit will be reverted." + exit 78 + fi + n=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq 'length') + echo "associated PRs: $n" + if [ "$n" -eq 0 ]; then + echo "::error::Direct push to main is not allowed. Open a PR (CI must pass, then merge). This commit will be reverted." + exit 78 + fi + - name: Revert the offending push + if: failure() + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + SHA: ${{ github.event.workflow_run.head_sha }} + run: | + git config --global user.name "ci-guard" + git config --global user.email "ci-guard@users.noreply.github.com" + git clone "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" repo + cd repo + git revert --no-edit "$SHA" || { echo "revert failed (merge conflict?) — MANUAL ACTION REQUIRED"; exit 1; } + git push origin main + echo "Reverted $SHA on main." diff --git a/.gitignore b/.gitignore index 263e93f..f01d5e9 100644 --- a/.gitignore +++ b/.gitignore @@ -123,6 +123,7 @@ dev-docs/ config/streams.json config/device-config.json config/contact-details.json +config/security.json config/web/ config/admin/ @@ -133,3 +134,15 @@ Desktop.ini # Windows reserved name (can appear as accidental file) nul +/release/ +.claude/settings.local.json +.claude-consult-prompt.md +# Root config/ holds per-install runtime state — anchored so src/config/ (source +# code) is never caught by this pattern (it silently blocked `git add` there). +/config/ +/config/binary-probe.json +/config/telemetry.json +electron/telemetry-key.json + +# Bundled FFmpeg/Icecast binaries for the installer (fetched, never committed) +/vendor/ diff --git a/CLAUDE.md b/CLAUDE.md index 78bfcaf..9fbd895 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,8 +4,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Context -**Project Type:** Node.js audio streaming server -**Key Technologies:** FFmpeg, Icecast, Express.js, Socket.io +**Project Type:** Electron desktop app (LAN audio streaming; Express server embedded in the Electron main process) +**Key Technologies:** Electron, FFmpeg, Icecast, Express.js, PostHog (voluntary telemetry) **Architecture:** - `src/services/StreamingService.js` → Core FFmpeg process management @@ -20,6 +20,24 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **dev** is the integration branch. All changes land here (via merge or PR) before they can go to main. - **Feature branches** are created from **main** (or **dev**) when starting new work. Pull from main into your feature branch if you need latest: `git fetch origin main && git merge origin/main` (or rebase). - **Flow:** main → create `feature/xyz` → work → **merge feature into dev locally** → test → push dev → merge dev into main (release). +- **main is now PR-protected (2026-10-28):** `enforce-prs.yml` auto-reverts any main commit that wasn't merged via a PR with green CI. Releases go dev → PR → main (pushing main directly gets reverted). dev stays open for the local-merge workflow above. + +### Pre-review gate for streaming features (enforced) + +Every PR that adds or changes streaming behavior (stream lifecycle, +proxying, device capture, listener fanout, start-all, queueing) must +answer the 7 hostile conditions in **docs/pressure-checklist.md** +(uneven load, slow-not-down dependency, 10x data, machine dies mid-write, +poor network, duplicate request, hostile traffic) in the PR description +for the changed code path. Adopted from the devops practices DB. + +### Integrity linter (CI-enforced) + +`node scripts/check-design-tokens.mjs` fails CI on NEW violations of the +UI rules below (hardcoded hex outside token sources, gray-* classes, +native dialogs, runtime CDN refs). Existing debt is grandfathered in +`scripts/design-token-baseline.json`; to accept new debt deliberately run +`--update-baseline` and say why in the commit. ### Merge feature into dev locally (recommended) @@ -54,6 +72,26 @@ Example: `git merge feature/start-all-and-unique-names`. After pushing dev, you - FFmpeg exit codes are unsigned, require conversion to signed - Example: 0xA7F00008 needs special handling +## UI rules (enforced) + +- **Design tokens are the single source of truth**: `public/assets/css/tokens.css` (`--tok-*`, dark default + `[data-theme="light"]`). Tailwind maps them (`tailwind.config.js` → `bg-surface-1`, `text-muted`, `border-line`, `text-accent`, `rounded-pill`, `shadow-glow`…). **Never hardcode hex colors or dark-only Tailwind grays in new UI code** — use the semantic tokens so both themes work. +- **Component showcase (dev-only) at `/showcase.html`**: every approved component variant, rendered from live tokens in both themes. New UI must match a pattern shown there — if it doesn't fit, extend the showcase first, then build. Check both themes on that page when touching tokens or `ui.css`. +- **Shared components live in `public/assets/css/ui.css`**: `.btn-primary/secondary/ghost/danger`, `.play-btn`, `.copy-btn` (IconButton), `.channel-badge`, `.stream-row`, `.live-chip`, `.eq`, `.mq` (marquee), `.tok-label`, `.toast`/`.toast-stack` (via `LSToasts`), `.stream-error` (collapsed row error), `.ls-collapse` (remembered collapsibles via `LSCollapse`). New variants need a shared definition there first — no one-off button styles. +- **Toasts must stack**: all transient notifications go through `LSToasts.show(message, type, {tag, linkUrl, linkText})` — it appends into `#toast-stack` so simultaneous toasts push down instead of overlapping; same-`tag` toasts replace each other. Never hand-roll `position:fixed` toasts. +- **One icon family**: Material Symbols Rounded, locally served. Icon-only buttons must have `aria-label` + `title`. Replace decorative emoji used as UI icons; content emoji is fine. +- **Sharing the listener page happens in ONE place** (header share card: open / copy / QR via `LSShare`). Never per-stream share buttons — we share the page, not individual mounts. +- **Notifications**: transient confirmations via the page toast; stream failures stay visible near the stream; never `alert()`. Don't report success before the server confirms. +- **Offline rule**: no runtime CDN dependencies — fonts/icons/styles all self-hosted (`build:css`, `vendor:fonts`). +- After changing classes anywhere: run `npm run build:css`. +- **No PWA on the listener page** (decision, 2026-10-28): installed shortcuts are bound to the origin they were made from, so a DHCP IP change kills every listener's shortcut. The page stays a plain bookmarkable URL; `streams.html` actively unregisters any leftover service worker. Don't re-add a manifest/SW without solving dynamic IPs first. + +## Telemetry (PostHog) + +- `src/services/TelemetryService.js` — opt-in only; **never** capture personal data, audio, stream names, or paths. Events: `app_start`, `stream_started` (format), `stream_stopped`, `stream_failed` (error category), `telemetry_enabled/disabled`. Distinct ID = anonymous install UUID from `config/telemetry.json`. +- Enabled only when user consented (first-run Electron prompt; toggle in tray menu) **and** `POSTHOG_KEY` env is set (`POSTHOG_HOST` optional). No-op otherwise — keep it that way for any new events. +- Skipped entirely under `NODE_ENV=test`. All calls are fire-and-forget with catch: telemetry must never break the app. +- Docs: README "Telemetry & Privacy" (user-facing) + this section (dev-facing). + ## Configuration - `.env` - Environment variables (use `env.example` as template) diff --git a/LANStreamer-redesign/README.md b/LANStreamer-redesign/README.md new file mode 100644 index 0000000..86e5091 --- /dev/null +++ b/LANStreamer-redesign/README.md @@ -0,0 +1,36 @@ +# LANStreamer — UI/UX redesign + +Open `dashboard.html` or `listener.html` in any browser (no build step). Use the moon icon to switch light/dark; resize to see mobile. + +Files +- `styles.css` — shared design tokens (light + dark) and components. Drop-in replacement for the current token sheet. +- `app.js` — theme memory, stacked/tagged toasts, icons. +- `dashboard.html` — admin dashboard (interactive mock: start/stop, filter, new-stream drawer, overflow menu, empty state). +- `listener.html` — public listener page. + +## What changed and why + +Admin dashboard +1. **Status strip at the top** — on-air count, listeners, Icecast status and "needs attention" are visible at a glance instead of buried in collapsed sidebar cards. +2. **Security banner is actionable** — plain-language risk + a direct "Change password" button, in warning (not error) colour so it doesn't compete with real stream failures. +3. **Live streams grouped first** — "On air" and "Ready" sections; no more hunting for the one live row among seven. +4. **Full source names** — the old rows truncated the *start* of device names ("tput (VB-Audio…"). Names now truncate at the end with a tooltip. +5. **Errors explain the fix** — the error row shows cause + "Change input" / "Retry" inline, instead of a collapsed "Audio Device Not Found" toggle. +6. **Fewer icons per row** — one play/stop button + a "⋯" menu (copy link, edit, duplicate, delete). Delete no longer sits one click from Play. Up/down arrows replaced by a drag handle on hover. +7. **Stop is visually distinct from Play** (red square vs neutral play), so the live row is unmistakable. +8. **New stream as a side drawer** — input list shows which device is already in use and a live level meter; quality chosen by purpose (Speech / Standard / Music); optional auto-start. +9. **Share card** — QR + copyable LAN address replaces the cramped "Listen to streams:" header pill. +10. **Audio inputs always visible** — status (in use / idle / missing) per device instead of a "Component unavailable" collapsed panel. +11. **Filters** (All / Live / Issues) and a friendlier empty state with a 3-step guide. + +Listener page +1. **One big play button** with the current channel, live timer, listener count, waveform and volume. +2. **Channel picker** for multi-room / multi-language events (designed for interpretation use). +3. **Help line** for the most common "no sound" causes. +4. **Mobile mini-player** stays pinned at the bottom after scrolling. +5. Event artwork is themed from tokens instead of a fixed illustration; About/Contact are native, keyboard-accessible disclosures. + +Design system +- Typography: Space Grotesk (UI) + JetBrains Mono (IDs, bitrates, timers). +- Warning colour now separate from error; `--onair` reserved for the ON AIR badge. +- Visible focus rings, `prefers-reduced-motion` respected, colour never the only status signal (text labels on every chip). diff --git a/LANStreamer-redesign/app.js b/LANStreamer-redesign/app.js new file mode 100644 index 0000000..a69aa4f --- /dev/null +++ b/LANStreamer-redesign/app.js @@ -0,0 +1,26 @@ +// Shared: theme toggle (remembered), toasts, icons +(function(){ + const saved = localStorage.getItem('ls-theme'); + const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches; + document.documentElement.dataset.theme = saved || (prefersDark ? 'dark' : 'light'); +})(); +window.LS = { + toggleTheme(){ + const t = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; + document.documentElement.dataset.theme = t; localStorage.setItem('ls-theme', t); + }, + toast(msg, tag){ + let box = document.querySelector('.toasts'); + if(!box){ box = document.createElement('div'); box.className='toasts'; box.setAttribute('aria-live','polite'); document.body.appendChild(box); } + if(tag){ const old = box.querySelector(`[data-tag="${tag}"]`); if(old) old.remove(); } + const t = document.createElement('div'); t.className='toast'; t.textContent=msg; if(tag) t.dataset.tag=tag; + box.appendChild(t); while(box.children.length>4) box.firstChild.remove(); + setTimeout(()=>t.remove(), 3200); + } +}; +const I = { + logo:'', + play:'', + stop:'', + pause:'', +}; diff --git a/LANStreamer-redesign/dashboard.html b/LANStreamer-redesign/dashboard.html new file mode 100644 index 0000000..12d13ed --- /dev/null +++ b/LANStreamer-redesign/dashboard.html @@ -0,0 +1,275 @@ + + + + +LANStreamer — Admin dashboard + + + + + + + +
+
LANStreamer/Sounds For A Beautiful Day
+
+ + Open listener page + + +
+ +
+
+
On air1/ 7 streams
+
Listeners23 peak 31
+
Icecast serverOnline:8000
+
Needs attention1 stream · 1 setting
+
+ + + +
+
+
+

Streams

+
+ +
+ + + +
+
+
+
+

No streams yet

+

Pick an audio input, name it, and go live — listeners on your Wi-Fi hear it instantly.

+
  1. 1New stream
  2. 2Choose input
  3. 3Go live
+
+

Tip: route app audio with VB-Audio Cable or Dante Virtual Soundcard.

+
+
+ + +
+
+ + + + +
+ + + + + diff --git a/LANStreamer-redesign/listener.html b/LANStreamer-redesign/listener.html new file mode 100644 index 0000000..532a115 --- /dev/null +++ b/LANStreamer-redesign/listener.html @@ -0,0 +1,130 @@ + + + + +Sounds For A Beautiful Day — Live + + + + + + + +
+
LANStreamer
+
+ ON AIR + +
+ +
+
+ Live now · on this network +

Sounds For A Beautiful Day

+

Livestreaming you into a beautiful day!

+
+ +
+
+
+ +
+
LIVE13m 53s
+

Main Room English

+
192 kbps · 18 listening
+ +
+
+
+ +
+

Choose a channel

+ + +
ESSpanish translationStarts soon
+
+

No sound? Make sure you're on the venue Wi-Fi and your phone isn't on silent. Keep this page open — audio continues with the screen locked.

+
+ + +
+
+ +
Main Room EnglishLIVE
+ + + + + + diff --git a/LANStreamer-redesign/styles.css b/LANStreamer-redesign/styles.css new file mode 100644 index 0000000..eee1b00 --- /dev/null +++ b/LANStreamer-redesign/styles.css @@ -0,0 +1,110 @@ +/* LANStreamer redesign — shared tokens + components */ +:root{ + --bg:#F4F5F7; --surface-1:#FFFFFF; --surface-2:#EEF0F3; --inset:#E6E9EE; + --text-1:#0E1420; --text-2:#5A6475; --text-3:#8A93A3; + --line:rgba(14,20,32,.09); --line-strong:rgba(14,20,32,.16); + --accent:#0A84D6; --accent-soft:rgba(10,132,214,.10); --on-accent:#fff; + --live:#14A44D; --live-soft:rgba(20,164,77,.12); + --warn:#D98A04; --warn-soft:rgba(217,138,4,.12); + --danger:#D93025; --danger-soft:rgba(217,48,37,.10); + --onair:#E5372B; + --radius:14px; --radius-sm:9px; + --shadow:0 1px 2px rgba(14,20,32,.04),0 8px 24px -12px rgba(14,20,32,.12); + --font:"Space Grotesk",system-ui,sans-serif; --mono:"JetBrains Mono",ui-monospace,monospace; +} +[data-theme="dark"]{ + --bg:#07090D; --surface-1:#0E1117; --surface-2:#141821; --inset:#0A0D12; + --text-1:#EEF1F6; --text-2:#97A0B0; --text-3:#636C7C; + --line:rgba(255,255,255,.07); --line-strong:rgba(255,255,255,.13); + --accent:#1AA6F5; --accent-soft:rgba(26,166,245,.13); --on-accent:#03121C; + --live:#2BD46A; --live-soft:rgba(43,212,106,.12); + --warn:#F5A524; --warn-soft:rgba(245,165,36,.13); + --danger:#F2555A; --danger-soft:rgba(242,85,90,.12); + --onair:#FF4A3D; + --shadow:0 1px 0 rgba(255,255,255,.03) inset,0 10px 30px -14px rgba(0,0,0,.7); +} +*{box-sizing:border-box;margin:0;padding:0} +html,body{background:var(--bg);color:var(--text-1);font-family:var(--font);font-size:15px;line-height:1.45;-webkit-font-smoothing:antialiased} +button{font:inherit;color:inherit;background:none;border:0;cursor:pointer} +a{color:var(--accent);text-decoration:none} +.mono{font-family:var(--mono);font-size:12px;letter-spacing:.01em} +.muted{color:var(--text-2)} .faint{color:var(--text-3)} +.wrap{max-width:1180px;margin:0 auto;padding:0 24px} +:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:8px} + +/* Top bar */ +.topbar{position:sticky;top:0;z-index:20;background:color-mix(in oklab,var(--bg) 82%,transparent);backdrop-filter:blur(12px);border-bottom:1px solid var(--line)} +.topbar .wrap{display:flex;align-items:center;gap:16px;height:64px} +.brand{display:flex;align-items:center;gap:10px;font-weight:600;font-size:17px;letter-spacing:-.01em;min-width:0} +.brand-mark{width:32px;height:32px;border-radius:9px;background:var(--accent);color:var(--on-accent);display:grid;place-items:center;flex-shrink:0} +.brand b{font-weight:700} +.brand .sep{color:var(--text-3);font-weight:400;margin:0 2px} +.brand .event{color:var(--text-2);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.spacer{flex:1} +.iconbtn{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;color:var(--text-2);position:relative;flex-shrink:0} +.iconbtn:hover{background:var(--surface-2);color:var(--text-1)} +.iconbtn .dot{position:absolute;top:7px;right:8px;width:7px;height:7px;border-radius:50%;background:var(--danger);box-shadow:0 0 0 2px var(--bg)} +.svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round} + +/* Buttons */ +.btn{display:inline-flex;align-items:center;gap:8px;height:36px;padding:0 14px;border-radius:10px;font-weight:600;font-size:14px;white-space:nowrap;border:1px solid transparent;transition:background .15s,border-color .15s} +.btn-primary{background:var(--accent);color:var(--on-accent)} +.btn-primary:hover{filter:brightness(1.08)} +.btn-secondary{background:var(--surface-1);border-color:var(--line-strong)} +.btn-secondary:hover{background:var(--surface-2)} +.btn-ghost{color:var(--text-2)} .btn-ghost:hover{background:var(--surface-2);color:var(--text-1)} +.btn-danger{color:var(--danger);border-color:color-mix(in oklab,var(--danger) 35%,transparent)} +.btn-danger:hover{background:var(--danger-soft)} +.btn:disabled{opacity:.45;cursor:not-allowed} +.btn-sm{height:30px;padding:0 10px;font-size:13px;border-radius:8px} + +/* Chips */ +.chip{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 9px;border-radius:999px;font-size:12px;font-weight:600;letter-spacing:.02em;white-space:nowrap} +.chip .pip{width:6px;height:6px;border-radius:50%;background:currentColor} +.chip-live{background:var(--live-soft);color:var(--live)} +.chip-live .pip{animation:pulse 1.6s infinite} +.chip-off{background:var(--surface-2);color:var(--text-2)} +.chip-err{background:var(--danger-soft);color:var(--danger)} +.chip-warn{background:var(--warn-soft);color:var(--warn)} +.chip-accent{background:var(--accent-soft);color:var(--accent)} +@keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in oklab,currentColor 60%,transparent)}70%{box-shadow:0 0 0 6px transparent}100%{box-shadow:0 0 0 0 transparent}} + +/* Cards */ +.card{background:var(--surface-1);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)} +.card-h{display:flex;align-items:center;gap:10px;padding:16px 18px;border-bottom:1px solid var(--line)} +.card-h h2,.card-h h3{font-size:15px;font-weight:600;letter-spacing:-.005em} +.card-b{padding:16px 18px} +.eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--text-3)} + +/* Equalizer */ +.eq{display:inline-flex;align-items:flex-end;gap:2px;height:12px} +.eq i{width:3px;background:currentColor;border-radius:1px;animation:eq 1s ease-in-out infinite} +.eq i:nth-child(1){height:40%;animation-delay:-.2s}.eq i:nth-child(2){height:100%;animation-delay:-.5s}.eq i:nth-child(3){height:65%;animation-delay:-.1s}.eq i:nth-child(4){height:85%;animation-delay:-.7s} +@keyframes eq{50%{transform:scaleY(.35)}} +.eq i{transform-origin:bottom} + +/* Toggle */ +.switch{width:38px;height:22px;border-radius:999px;background:var(--inset);border:1px solid var(--line-strong);position:relative;transition:.2s;flex-shrink:0} +.switch::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--surface-1);box-shadow:0 1px 2px rgba(0,0,0,.25);transition:.2s} +.switch[aria-checked="true"]{background:var(--accent);border-color:var(--accent)} +.switch[aria-checked="true"]::after{left:18px} + +/* Inputs */ +.field{display:flex;flex-direction:column;gap:6px} +.field label{font-size:13px;font-weight:600} +.field .hint{font-size:12px;color:var(--text-3)} +.input,.select{height:40px;padding:0 12px;border-radius:10px;background:var(--inset);border:1px solid var(--line-strong);color:var(--text-1);font:inherit;font-size:14px;width:100%} +.input:focus,.select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)} + +/* Toast */ +.toasts{position:fixed;right:20px;bottom:20px;display:flex;flex-direction:column;gap:8px;z-index:60} +.toast{display:flex;align-items:center;gap:10px;min-width:260px;max-width:360px;padding:12px 14px;border-radius:12px;background:var(--text-1);color:var(--bg);font-size:14px;box-shadow:0 12px 30px -10px rgba(0,0,0,.4);animation:tin .25s ease} +@keyframes tin{from{transform:translateY(8px);opacity:0}} + +/* Footer */ +.foot{border-top:1px solid var(--line);margin-top:48px} +.foot .wrap{display:flex;flex-wrap:wrap;gap:12px 20px;align-items:center;padding:20px 24px;font-size:13px;color:var(--text-3)} +.foot a{color:var(--text-2)} .foot a:hover{color:var(--accent)} + +@media (max-width:720px){.wrap{padding:0 16px}.hide-sm{display:none!important}} +@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}} diff --git a/README.md b/README.md index 0a49eb8..edf6bd6 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,156 @@ # LANStreamer -A comprehensive solution that turns a standard PC into a multi-channel audio streaming server for local area networks (LAN). This project provides a **web-based application** that orchestrates **FFmpeg** and **Icecast** to deliver local, live, low-latency audio broadcasts for events like language interpretation, meetings, or conferences. +**LAN audio streaming for language interpretation, meetings, and events.** LANStreamer is a desktop app that turns an ordinary PC into a multi-channel audio streaming server for your local network (LAN) — it orchestrates **FFmpeg** and **Icecast** behind the scenes, so a whole room can listen to live, low-latency audio on their phones. -## 📋 Documentation - -| Documentation | Description | -|--------------|-------------| -| **Quick Start** | This README - get up and running quickly | -| **[Startup sequence](docs/STARTUP-SEQUENCE.md)** | Step-by-step process order for `npm run dev`; troubleshoot each step | -| **[Troubleshooting](docs/TROUBLESHOOTING.md)** | Common issues: network, firewall, playback problems | -| **[System Architecture](docs/LANSTREAMER_SYSTEM_ARCHITECTURE.md)** | ⭐ Complete technical documentation - all services, flows, and architecture | -| [Installation Guides](docs/guides/README.md) | FFmpeg, Icecast setup guides | -| [Network Setup](docs/NETWORK-SETUP.md) | Static IP configuration for events | -| [Developer Guide](CLAUDE.md) | Architecture and contribution guide | +> **Status: v2.0.0 is released** — download the installer or standalone build from [GitHub Releases](https://github.com/jerryagenyi/LANStreamer/releases) (both bundle FFmpeg + Icecast; nothing to pre-install). Running from source still works: `npm run electron:dev`, or the server-only `Start LANStreamer Server.bat`. Release process and pre-ship checks: [docs/GO-LIVE-CHECKLIST.md](docs/GO-LIVE-CHECKLIST.md). --- -## 🚀 Quick Start +## 🎯 What is LANStreamer? + +Think "wireless interpretation booth": the interpreter's PC captures audio (microphone, audio interface, or virtual audio cable) and broadcasts it over Wi-Fi. Anyone on the same network opens a link in their browser and listens — nothing to install. + +### Interfaces + +| Interface | Purpose | Access | +|-----------|---------|--------| +| **Admin Dashboard** | Control center for managing streams | `http://localhost:3001/dashboard` on the operator PC (login: `admin` / `lanstreamer123` by default) | +| **Listener Interface** | User-friendly page for browsing and playing streams | `http://YOUR-PC-IP:3001/streams` (public, no login) | + +### Use Cases + +- **Language Interpretation**: Multiple language channels for conferences +- **Meeting Audio**: Professional audio distribution for events +- **Training Sessions**: Audio streaming for educational content +- **Local Broadcasting**: Any situation requiring reliable, low-latency audio distribution -### For Non-Technical Users +--- -**Step 1: Install Prerequisites** -- **Node.js**: Download from https://nodejs.org/ (choose "LTS" version) -- **FFmpeg & Icecast**: See [Installation Guides](docs/guides/README.md) +## 🚀 Quick Start (Desktop App) -**Step 2: Download & Extract** -1. Go to https://github.com/jerryagenyi/LANStreamer -2. Click **"Code"** → **"Download ZIP"** -3. Extract the ZIP file and open the `LANStreamer` folder +**Step 1: Launch LANStreamer** +Start the LANStreamer app on the operator PC. It starts the streaming server and opens the dashboard window automatically — no terminal windows to babysit. Closing the window only hides it to the tray: streams keep running for your listeners. To stop for real, use the tray icon → **Stop All & Quit**. -**Step 3: One-Click Start** -1. Double-click `Start LANStreamer Server.bat` -2. Wait for dependencies to install and the server to start -3. Note the URLs shown in the terminal +> No app package? You can run the same server from a source/ZIP copy: double-click `Start LANStreamer Server.bat` (requires [Node.js](https://nodejs.org/) LTS, plus FFmpeg & Icecast — see the [Installation Guides](docs/guides/README.md)). Building the desktop package from source is covered in the [Developer Guide](docs/README-DEVELOPERS.md). -**Step 4: Access Your Streaming Server** -- **Admin Dashboard**: `http://YOUR-IP:3001` (login required) -- **Listener Page**: `http://YOUR-IP:3001/streams` (public access) +**Step 2: Sign In** +The dashboard asks you to sign in when it opens. -**Default Admin Credentials** (change these immediately in `.env`): - **Username**: `admin` - **Password**: `lanstreamer123` -### For Technical Users +Change these before your event — see [Security Setup](#-security-setup--admin-login) below. -```bash -# Clone and install -git clone https://github.com/jerryagenyi/LANStreamer.git -cd LANStreamer -npm install +**Step 3: Create a Stream** +Click **Add stream**, pick an audio source (your microphone, audio interface, or virtual audio cable), choose a quality, and click **Go live**. Repeat for more channels (e.g. one per language). You can also use **Start All / Stop All**. -# Start the server -npm start -``` +**Step 4: Share with Listeners** +The dashboard's **Share with listeners** card shows the listener link (e.g. `http://192.168.x.x:3001/streams`). Copy it, or tap the QR icon and let people scan it. Share it once — by chat, email, projector slide, or QR on paper. -Then visit `http://localhost:3001` for the admin dashboard. +--- -> **Need more detail?** See [System Architecture](docs/LANSTREAMER_SYSTEM_ARCHITECTURE.md) or [Installation Guides](docs/guides/README.md) +## 🎧 For Listeners + +1. Join the **same Wi-Fi/network** as the operator PC +2. Open the shared link (or scan the QR code) — no app, no account, no login +3. Tap the stream they want and press play + +The listener page is forgiving: it **auto-resumes** a stream that drops and comes back on air (unless they paused it themselves), and on reload it remembers the stream they played last. --- -## 🎯 What is LANStreamer? +## 🔐 Security Setup — Admin Login -LANStreamer creates a complete audio streaming ecosystem with two main interfaces and three core components: +**Default admin credentials:** -### Interfaces +| Setting | Value | +|---------|-------| +| Username | `admin` | +| Password | `lanstreamer123` | -| Interface | Purpose | Access | -|-----------|---------|--------| -| **Admin Dashboard** | Control center for managing streams | `http://localhost:3001/` (auth required) | -| **Listener Interface** | User-friendly page for browsing and playing streams | `http://localhost:3001/streams` (public) | +Sign in at `http://localhost:3001/dashboard` (or the Electron app window) — the login appears automatically when the dashboard is opened. The **listener page (`/streams`) has no login**; phones on the LAN just open the shared link. -### Core Components +**First run:** while the default credentials are active, the dashboard shows a banner stating them and how to change them — it disappears permanently once you set your own. -1. **Audio Input** - Any detected audio device (microphone, audio interface, virtual audio cable) -2. **FFmpeg** - Reads audio channels, encodes, and pushes to streaming server -3. **Icecast** - Open-source streaming server that broadcasts to network clients +**Forgot your password?** On the operator PC, delete the admin login file and restart LANStreamer — the default login above returns, and you can set a new one from the dashboard (Admin account). Installed app: `%APPDATA%\LANStreamer\security.json` · running from source: `config\security.json`. (The login page carries this same hint — it only opens on the operator's PC, so the path is safe to show there.) -### Use Cases +Admin access has two layers: -- **Language Interpretation**: Multiple language channels for conferences -- **Meeting Audio**: Professional audio distribution for events -- **Training Sessions**: Audio streaming for educational content -- **Local Broadcasting**: Any situation requiring reliable, low-latency audio distribution +1. **Login (username + password)** — every admin API call requires a session token from signing in. +2. **Localhost lock** — the dashboard and admin APIs refuse connections from other machines, so even someone on your LAN with the password cannot reach admin. ---- +To change the defaults, use the in-app settings: dashboard → sidebar → **Admin account** card → enter the current password and a new username/password → **Update login**. It applies immediately (no restart), signs out every other session, and is stored in the app's data folder — this works in the packaged desktop app, where there is no `.env` to edit. -## 📁 Project Structure +Running from source, `.env` also works — copy `env.example` to `.env`, then edit: +```env +ADMIN_USERNAME=your-new-username +ADMIN_PASSWORD=your-strong-password +JWT_SECRET=your-jwt-secret +SESSION_SECRET=your-session-secret ``` -LANStreamer/ -├── Start LANStreamer Server.bat # Windows startup (double-click to start) -├── Update LANStreamer.bat # Update script -├── src/ # Backend (Node.js/Express) -├── public/ # Frontend (web interface) -├── config/ # Runtime config (auto-generated) -├── docs/ # Documentation -└── manual-setup/ # Hardware-specific guides -``` -> **For developers:** See [CLAUDE.md](CLAUDE.md) for detailed architecture and code structure. +Restart LANStreamer afterwards. (The in-app change takes precedence over `.env` values. Logins stay valid for 7 days, or set `JWT_EXPIRES_IN` to change that.) + +**Icecast (the streaming server LANStreamer drives) is hardened automatically when bundled.** The bundled Icecast binds to `127.0.0.1` only — listeners receive audio through LANStreamer's own play proxy on port 3001, so nothing about Icecast is ever exposed to your network — and its passwords are randomly generated per install (the app reads them itself; nobody ever types them). Someone who knows LANStreamer's defaults cannot reach or control it. + +Using your **own Icecast install** instead of the bundled one? That install is on your network as you configured it: if it still has Icecast's stock default passwords, the dashboard shows a security warning until you change them (edit `icecast.xml` in the install folder, e.g. `C:\Program Files (x86)\Icecast\icecast.xml` — the `` block — then restart LANStreamer). --- ## ⚙️ System Requirements -- **Operating System**: Windows 10/11 (macOS/Linux supported) -- **Node.js**: Version 18 or higher ([Download](https://nodejs.org/)) -- **FFmpeg**: Audio/video processing toolkit ([Download](https://ffmpeg.org/)) -- **Icecast**: Streaming media server ([Download](https://icecast.org/)) -- **Audio Input**: Any audio device (built-in mic, USB mic, audio interface, etc.) +**Minimum (operator PC)** +- Windows 10/11 64-bit (macOS/Linux supported from source) +- 4 GB RAM, dual-core CPU — encoding one stream uses ~2–5% of a modern CPU +- Any audio input (built-in mic, USB, or virtual audio cable) +- FFmpeg + Icecast — **bundled with the desktop installer** (in preparation). Running from source/CLI? Install them separately (see [guides](docs/guides/README.md)) + +**Listeners:** any phone/PC with a browser and WiFi — nothing to install. ~20–25 listeners per consumer WiFi access point for stable audio; larger events should add access points. + +**Recommended:** 8 GB RAM for 4+ simultaneous streams, wired Ethernet from the operator PC to the router, 5 GHz WiFi for listeners (see [Latency guide](docs/LATENCY.md)). --- -## 🔐 Security Setup +## 🛠️ Troubleshooting + +**Common Issues:** -**⚠️ CRITICAL: Change Default Credentials Immediately!** +| Problem | Quick Fix | +|---------|-----------| +| Stream won't start | Make sure the audio device isn't already in use by another app; rescan devices in the dashboard | +| Listeners can't connect | Check everyone is on the **same Wi-Fi/network** as the operator PC, and the link uses the PC's LAN IP (not `localhost`). Also see [Firewall Issues](docs/TROUBLESHOOTING.md#firewall-issues) | +| "This site can't be reached" | Likely subnet mismatch — see [Network Troubleshooting](docs/TROUBLESHOOTING.md#network-connectivity-issues) | +| "Port 3001 already in use" | A leftover server instance is running — close its terminal window (or quit the app from the tray), or end `node.exe` in Task Manager, then relaunch. The dashboard's stop buttons end streams and Icecast, but no button can stop the server itself | +| "No audio devices detected" | Ensure device is connected and not in use by another app | +| "Stream format not supported" | See [Stream Playback Issues](docs/TROUBLESHOOTING.md#stream-playback-issues) | +| Link/QR stopped working after moving networks | The share card always shows the PC's **current** network address — re-share the link/QR after the network changes; old links point at the old address | +| Audio reaches listeners ~1–1.5 s after the room | Normal — the low-latency player holds a small buffer that also absorbs Icecast's ½-second delivery rhythm; how it works and how to tune it: [docs/LATENCY.md](docs/LATENCY.md) | -1. Copy `env.example` to `.env` -2. Edit `.env` and change: - - `ADMIN_USERNAME=your-new-username` - - `ADMIN_PASSWORD=your-strong-password` - - `JWT_SECRET=your-jwt-secret` - - `SESSION_SECRET=your-session-secret` -3. Restart LANStreamer +> **📖 In-app guide:** LANStreamer ships with a built-in troubleshooting page at `http://localhost:3001/docs` — open it from any browser on the operator PC. For the full guide see [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) (subnet issues, firewall configuration, network diagnostics). --- ## 🔄 Updating -**For ZIP Users:** -```bash -# Double-click this file in your installation folder -Update LANStreamer.bat -``` +**Packaged installs (once released):** download the new installer from [GitHub Releases](https://github.com/jerryagenyi/LANStreamer/releases) and run it — your stream configuration is kept. -**For Git Users:** -```bash -git pull origin main -npm install # If dependencies changed -``` +**ZIP/source installs:** double-click `Update LANStreamer.bat` in your installation folder. --- -## 🛠️ Troubleshooting +## 📊 Telemetry & Privacy -**Common Issues:** +LANStreamer is **free and open source — no one makes money from it, and no one should** (MIT license). -| Problem | Quick Fix | -|---------|-----------| -| "npm not found" | Install Node.js from [nodejs.org](https://nodejs.org/) | -| "Port 3001 already in use" | Close other apps using port 3001, or change `PORT` in `.env` | -| "This site can't be reached" | Check [Network Troubleshooting](docs/TROUBLESHOOTING.md#network-connectivity-issues) - likely subnet mismatch | -| "Connection refused" | Check [Firewall Setup](docs/TROUBLESHOOTING.md#firewall-issues) | -| "No audio devices detected" | Ensure device is connected and not in use by another app | -| "Stream format not supported" | See [Stream Playback Issues](docs/TROUBLESHOOTING.md#stream-playback-issues) | +**Telemetry is ON by default (opt-out).** **What is collected:** anonymous events — app start, stream started/stopped/failed (with the error *category* only), app version, OS, and a random install ID. Every event also carries a fixed `app: lanstreamer` tag (the maintainer's PostHog project is shared across apps; the tag separates this app's events). **Never collected:** audio, stream names, contact details, file paths, IPs, or any personal data. + +**Listener failure reports:** when playback fails on a listener's device (audio error, buffer stall, the stream going off-air mid-listen, or the page failing to load), the listener page reports it to the *operator's own server*, which relays it to PostHog **only while the operator's telemetry is on** — carrying the stream label (e.g. "S1"), the media error code, and the browser platform. Listener devices never talk to PostHog directly and never hold any key; with telemetry off, the reports are dropped at the server. -> **📖 Full Troubleshooting Guide:** See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for detailed solutions including subnet issues, firewall configuration, and network diagnostics. +- **First run** shows a plain notice that statistics are on, what they contain, and a one-click **Turn it off**. +- **Change it anytime**: dashboard → sidebar → **Admin account** → *Anonymous usage statistics* switch, or tray icon → **Telemetry → On/Off**. +- Events go to the maintainer's PostHog over the internet, queued and flushed in the background — nothing is sent while telemetry is off, and a fully offline LAN works either way. +- Maintainers packaging a distributed build: run `node scripts/prepare-telemetry-key.mjs` before building — it bundles the PostHog key into the app (gitignored) so other operators' installs report too (each with their own off switch). --- @@ -164,24 +158,19 @@ npm install # If dependencies changed | Document | Description | |----------|-------------| -| **[Startup sequence](docs/STARTUP-SEQUENCE.md)** | Process order for `npm run dev`; troubleshoot step-by-step | +| [In-app Guide](http://localhost:3001/docs) | Troubleshooting & guide page built into the app | | **[Troubleshooting Guide](docs/TROUBLESHOOTING.md)** | Common issues and solutions (subnet, firewall, playback) | -| [Network Setup](docs/NETWORK-SETUP.md) | Static IP configuration for live events | | [Installation Guides](docs/guides/README.md) | FFmpeg and Icecast setup guides | -| [Comprehensive Docs](docs/LANStreamer-Documentation.md) | Complete technical documentation | -| [Developer Guide](CLAUDE.md) | Architecture and development workflow | +| [Network Setup](docs/NETWORK-SETUP.md) | Static IP configuration for live events | +| [Latency Guide](docs/LATENCY.md) | Getting the lowest latency for listeners | | [Manual Setup](manual-setup/README.md) | Hardware-specific configuration guides | | [Changelog](docs/CHANGELOG.md) | Version history and release notes | --- -## 🤝 Contributing - -We welcome contributions! See [CLAUDE.md](CLAUDE.md) for the development workflow and architecture guide. - -**Branch workflow:** We never add to `main` directly. Pull from `main` when starting work; do work on a feature branch; merge into `dev` first; only then does change go to `main`. See [CLAUDE.md](CLAUDE.md) for the full flow. +## 🧑‍💻 For Developers -For bug reports, include your OS, Node.js version, and steps to reproduce. +Building from source, running the test suite, architecture notes, and the contribution workflow (branch model, CI, UI rules, telemetry internals) all live in the **[Developer Guide](docs/README-DEVELOPERS.md)**. Preparing a release? Follow **[docs/GO-LIVE-CHECKLIST.md](docs/GO-LIVE-CHECKLIST.md)** (test matrix, packaging, versioning, GitHub release notes). See also [CLAUDE.md](CLAUDE.md) for enforced code patterns. --- @@ -193,9 +182,9 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🆘 Support -1. Check the [Troubleshooting Guide](docs/TROUBLESHOOTING.md) first -2. Review the [Installation Guides](docs/guides/README.md) -3. See [Comprehensive Documentation](docs/LANStreamer-Documentation.md) for advanced topics +1. Check the in-app guide at `http://localhost:3001/docs` +2. See the [Troubleshooting Guide](docs/TROUBLESHOOTING.md) +3. Review the [Installation Guides](docs/guides/README.md) 4. Create an issue on GitHub with detailed information --- diff --git a/TODO.md b/TODO.md index 13fa6e5..6f7c367 100644 --- a/TODO.md +++ b/TODO.md @@ -1,66 +1,49 @@ # LANStreamer – TODO -Context: [CLAUDE.md](CLAUDE.md), [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Delete when done. +Context: [CLAUDE.md](CLAUDE.md), [docs/APP-FLOW.md](docs/APP-FLOW.md), [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Delete items when done. --- ## To do (priority order) -- [ ] **Explore: Listener song requests** — Extend LANStreamer so anyone on the network (mobile or PC) can request to play a song from their device; admin grants the request and the song is listed on the listener page for others to hear. Support multiple user requests. +- [x] **Latency management — SHIPPED (2026-10-30)** — MSE listener player (~0.5 s buffered-ahead vs ~2.3 s browser default), FFmpeg low-delay flags, proxy no-Nagle/flushHeaders, 16 KB icecast burst. Measured: `scripts/verify-latency.mjs`. Remaining nice-to-have: "latency tips" link on the dashboard pointing at the operator-side guidance (5 GHz, dedicated AP, QoS, wired operator); sub-second would need a WebSocket/WebRTC + Opus transport rewrite (deferred — see docs/LATENCY.md). -- [ ] **Centralize notifications/alerts** — Control all notifications, alerts, and toasts from a single place so one change impacts everywhere (e.g. styling, z-index, behaviour). To be done on branch `fix/error-handling-refactor` or a new dedicated branch (e.g. `refactor/centralize-notifications`). +- [ ] **Minimum PC specs + cross-OS test story** — README section: minimum requirements (x64, 4 GB RAM, Node 18+ for source runs, FFmpeg, Icecast; packaged app drops the Node requirement). Cross-OS testing: GitHub Actions windows/macos/ubuntu matrix runs the suite + `electron:smoke` online (Phase 4); locally, Playwright covers the web pages on all three OSes; no credible downloadable "test on every PC" tool exists beyond VMs + the CI matrix. -- [ ] **Centralised theming (light/dark, themes)** — UI-UX v2 was implemented with many styles inline in `index.html` and `streams.html` (duplicated variables, hardcoded colours). Refactor to a single theme layer (e.g. shared CSS or design tokens) so themes and light/dark mode can be added without touching every component. Prefer one source of truth for colours, shadows, and glass tokens. +- [ ] **UI redesign — dashboard deep pass** — Restyle remaining dashboard sections (Icecast panel, event/contact panels, header) to the broadcast language on tokens; a11y sweep (aria, focus rings, 44px targets). -- [x] **Lock admin to localhost** — Admin UI only on localhost; listeners use LAN IP. Implemented: `requireLocalhostAdmin` middleware; listener page and APIs (config, status, play, contact-details) allowed from LAN; integration tests in `lock-admin-localhost.test.js`. +- [ ] **Centralize notifications/alerts** — All toasts/modals from one module so styling + z-index + behaviour change in one place. -- [x] **Verify 5+ streams** — Confirmed 6/6 live (VB-Cable A/B, mics). Root cause of "5th fails" was bad device choice, not Icecast limit. +- [ ] **Internet broadcast tunnel (explore; only if feasible)** — Optional outbound-only tunnel so listeners outside the LAN can listen (e.g. self-hosted frp/ngrok-style relay on a VPS the operator controls). Must stay opt-in, off by default — LANStreamer's core is local, low-latency, private. Research latency + NAT constraints before committing. -- [x] **Unique stream names** — No duplicate display names (create/update); case-insensitive, trimmed. +- [ ] **Explore: Listener song requests** — Anyone on the network can request a song; admin grants; listed on listener page. (From PR #12.) -- [x] **Start/Stop All button** — Start all stopped/error streams; Stop all running; API `POST /api/streams/start-all`; partial-failure toast with names + first error; 150ms delay between success/error toasts. +- [ ] **Phase 2: cross-platform abstraction** — Device discovery per OS (dshow/avfoundation/pulse; StreamingService hardcodes dshow), config/logs in OS app-data dirs, retire .bat scripts. -- [x] **Listener page uses LAN IP** — Copy URLs and listener page use `config.host` from getPreferredLANHost / icecast.xml. Docs: "Which IP is my LAN / WiFi?" in TROUBLESHOOTING.md. +- [ ] **Phase 3: bundle binaries — NEARLY DONE (2026-10-30)** — vendor/ (gyan ESSENTIALS ffmpeg + Icecast kit, stock-parity per the §0.1 decision) + extraResources + data-dir runtime + first-run functional probe (BinaryProbeService) all live; `isIcecastRunning` now credential-verified (zombie-proof). Remaining: pin SHA256 checksums, official Icecast 2.5.0 kit download (2.4.4 EOL 2026-12-31; portable 2.5.0 already validated at ~/Tools/icecast-2.5.0), per-install admin values, GPL LICENSES files inside the installer, download-on-demand UI for probe failures. -- [x] **Mobile** — Listener page, "Listen to Streams" header link, and Copy URL use correct LAN IP. HeaderComponent fetches config.host; TROUBLESHOOTING.md "Action Required: Mobile Listener Setup". _QA: Test with real device on same WiFi._ +- [ ] **Phase 4: test/CI** — Playwright `_electron` + listener-page web E2E + axe-core + GitHub Actions 3-OS matrix (use `npm run electron:smoke`); electron-updater replaces UpdateService/.bat at packaging. Add E2E regression: machine IP change must not break status polling (covered by localhost polls; test the contract). -- [x] **Workflow/docs** — Branch workflow: feature → merge into dev locally → push dev → PR dev→main (remote only). Local branches: dev + features only; `main` deleted locally (remote-only). AGENTS.md removed; refs point to CLAUDE.md. PR #6 updated with lock-admin-localhost changes. - -- [x] **Notification types** — For duplicate stream name or duplicate source, use a simple modal ("stream already exists") without troubleshooting link; for real failures, existing error handling with diagnosis/link. Implemented in FFmpegStreamsManager: isDuplicateError(), showDuplicateModal(); used for start, restart, update, start-all. - -- [x] **Source validation & clear errors** — On stream failure, backend errorDiagnostics provides category (device, connection, auth, etc.), title, solutions; API returns shortMessage; admin dashboard shows error + troubleshooting link. Optional "Test source" button deferred. - -- [x] **Timing of Modals** — Alert modals should appear only after the action is performed, not before. All success modals now have 1-second delay after UI updates. - -- [x] **Listener: dismiss without reload** — New-stream notification dismisses overlay without full reload (don't stop playback). Hot reload only for the affected stream. Implemented: `dismissNewStreamNotification()` calls `loadStreams()` to refresh without page reload. - -- [x] **Error alert UX** — Structured error from API (shortMessage = diagnosis title); admin dashboard shows centred toast with message + link to troubleshooting guide; Start All partial failure lists failed names (up to 5 + "and N more") and first error. Duplicate errors use simple modal without link. - -- [x] **Stability** — Confirm 3+ streams stay stable (see TROUBLESHOOTING.md). _CONFIRMED working._ - -- [x] **Device change mid-stream / Play after edit** — Play proxy now checks Icecast response status; non-200 returns 502 with clear message so listener page no longer shows "Authentication Failed". Listener page shows "Stream not running. Start it on the dashboard, then try Play again." No refresh or delete/recreate needed. _See TROUBLESHOOTING.md §1c._ - -- [x] **Stream labels (optional)** — Streams show S1, S2, S3 from server order; label in `getStats()` and on listener page / dashboard. - -- [x] **Sortable streams (optional)** — Up/down buttons on dashboard; `POST /api/streams/reorder`; order persisted in `config/streams.json` (`_order`); listener page uses same order and S1/S2/S3 labels without refresh. - -- [x] **Contact: WhatsApp** — Country code enforced (digits 10–15, no leading zero); validation backend + ContactManager; `wa.me/` built on listener page; placeholder "e.g. +44 7123 456789 (country code required)". Not working - -- [x] **Update notification UX** — Bell icon in header; auto-check on load (HeaderComponent.checkForUpdates); when updateAvailable=true bell shows badge and "Update Available"; click opens modal with update info and link to release. Single button id header-update-bell-btn; manual check on click when no update yet. - -- [x] **Event Details & Your Contact components** — Not working as they should; details not saving for contact, and the event details collapsible doesn't open after saving details. - -- [x] **UI/UX polish (optional)** — [docs/UI-UX-RECOMMENDATIONS.md](docs/UI-UX-RECOMMENDATIONS.md). +- [ ] **Phase 6: packaging** — Windows NSIS/portable first; unsigned DMG later for savvy Mac users. Telemetry disclosure + LICENSES in installer. Telemetry key ships via `electron:build:win` (auto-injected from POSTHOG_KEY; also reads ~/.secrets/posthog on dev machines). --- -## Reference +## Done (2026-10-28 revival) -- Run `npm test` before releases; [STARTUP-SEQUENCE.md](docs/STARTUP-SEQUENCE.md) for troubleshooting. -- **Future refactor:** ErrorFactory consistency, remove/trim FFmpegService.js, device mapping extraction, split large HTML/JS — see inline notes in codebase. +- **Listener page redesigned** — broadcast rows + persistent mini-player (eq bars, both themes, a11y labels); theming foundation (tokens.css, dark/light toggle, pill buttons, logo variants). +- **Stale-stream / can't-create-stream fixed** — starting a stream now auto-starts Icecast (was: wall-of-text "cannot connect" error). Troubleshooting doc updated. +- **Doubled audio devices fixed** — FFmpeg 9 changed device-listing format (`[dshow @]`→`[in#0 @]`); parsers updated for both formats + PowerShell fallback output de-duplicated. (Note: `[^]]` regex classes mis-parse on this Node — always use `[^\]]`.) +- **Add-stream modal decluttered** — Pro-Tip/Virtual-Audio notices moved behind an info icon popover (links to manual-setup docs); redundant "Maximum 50 characters" hints removed (counter exists). +- **Delete-stream toast** — waits 900ms, verifies the stream is actually gone via status API, then shows `Stream "" deleted`. +- **Electron: external links** (e.g. "Listen to Streams") open in the system browser. +- **Telemetry key for packaged builds** — `electron:build:win` injects POSTHOG_KEY from the build environment into the app (scripts/prepare-telemetry-key.mjs); resolution order env → config/telemetry.json → bundled. +- Reconciled dev/main; merged error-handling branch (68/68 tests); branch cleanup (local = dev only). +- Dead code removed (SystemService, WebSocketService, FFmpegService); offline UI (vendored Tailwind + fonts, CSP 'self'). +- Phase 1 Electron wrapper (tray, Stop All & Quit, tree-kill, --smoke); stale-hostname bug fixed (localhost polls + hostname auto-sync); log spam fixed; dismissible notices; SVG logo set; env.example; repo cleanup; docs index. --- -## Earlier (historical) +## Reference -Notification timing, LANStreamer.bat updater, Claude hooks, custom-instructions, capacity NaN fix, config capacity fields, shortMessage + troubleshooting link. +- Run `npm test` before releases; `npm run build:css` after changing Tailwind classes. +- Versions (2026-10): FFmpeg 9.0.2 latest (app runs on 8.x, fine); Icecast 2.5.0 latest — **2.4.4 EOL 2026-12-31**. diff --git a/analysis_output/README.md b/analysis_output/README.md deleted file mode 100644 index 408af6a..0000000 --- a/analysis_output/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# analysis_output - -Historical regression analysis for LANStreamer v1.2.0 → v1.2.2 → v1.2.3. - -- **REGRESSION_ANALYSIS_SUMMARY.md** — Root cause summary (port 8000 vs 8001), timeline, and findings. Raw diffs and commit lists were removed during cleanup; see this file for the analysis. diff --git a/analysis_output/REGRESSION_ANALYSIS_SUMMARY.md b/analysis_output/REGRESSION_ANALYSIS_SUMMARY.md deleted file mode 100644 index a652e00..0000000 --- a/analysis_output/REGRESSION_ANALYSIS_SUMMARY.md +++ /dev/null @@ -1,193 +0,0 @@ -# LANStreamer Regression Analysis: v1.2.0 → v1.2.2 → v1.2.3 - -**Analysis Date:** 2026-01-26 -**Branch:** `analysis/regression-v1.2.0-to-v1.2.3` -**Status:** ✅ ROOT CAUSE IDENTIFIED - ---- - -## Executive Summary - -**ROOT CAUSE FOUND:** The regression from v1.2.2 was caused by a **hardcoded port change** from `8000` to `8001` in the FFmpeg Icecast URL. This was subsequently fixed in v1.2.3 by reverting to port `8000`. - -### Timeline of Events - -| Version | Port | StreamingService.js Line 178 | Status | -|---------|------|------------------------------|--------| -| v1.2.0 | `8000` | `icecast://source:hackme@localhost:8000/` | ✅ Working | -| v1.2.2 | `8001` | `icecast://source:hackme@localhost:8001/` | ❌ **BROKEN** - Connection refused | -| v1.2.3 | `8000` | `icecast://source:hackme@localhost:8000/` | ✅ Fixed | - -### Commit Evidence - -**v1.2.2 Release Commit:** `d3ddd9b` -- "Release v1.2.2: Critical fixes for port conflicts and streaming stability" - -**v1.2.3 Port Revert Commit:** `0dc9916` -- "fix: Revert to Icecast default port 8000, improve connection error message" - ---- - -## Detailed Analysis - -### 1. Critical Files Changed - -#### StreamingService.js (Primary Regression Source) - -**v1.2.0 → v1.2.2 Changes (490 lines):** -- **Line 178:** Port changed from `8000` to `8001` (BROKEN) -- Added Icecast status checking before stream start -- Added stderr capture for error diagnostics -- Added immediate crash detection -- Added Windows crash exit code handling (0xA7F00008) -- Added enhanced error messages with dependency chains - -**v1.2.2 → v1.2.3 Changes (46 lines):** -- **Line 178:** Port reverted from `8001` to `8000` (FIXED) -- Added AudioDeviceService cache lookup for device names -- Added VB-Audio Virtual Cable variants -- Enhanced connection error messages with port mismatch detection - -### 2. errorDiagnostics.js Finding - -**CRITICAL DISCOVERY:** `errorDiagnostics.js` did NOT exist in v1.2.0, v1.2.2, or v1.2.3. - -- Added **after** v1.2.3 in commit `3732460` -- Therefore, it **cannot be the cause** of the v1.2.0 → v1.2.3 regression -- This file was added as part of post-v1.2.3 troubleshooting improvements - -### 3. Dependency Analysis - -**package.json comparison:** -- v1.2.0: `"version": "1.2.0"` -- v1.2.2: `"version": "1.2.2"` -- v1.2.3: `"version": "1.2.3"` - -**No dependency changes detected.** All runtime dependencies remained identical across versions. - -### 4. Other File Changes - -**IcecastService.js:** Only 3 lines changed (minor adjustments) - -**FFmpegStreamsManager.js:** Minor UI improvements and timeout adjustments - ---- - -## Root Cause Analysis - -### The Bug - -```javascript -// v1.2.2 - BROKEN -const icecastUrl = `icecast://source:hackme@localhost:8001/${streamId}`; - -// v1.2.0 and v1.2.3 - WORKING -const icecastUrl = `icecast://source:hackme@localhost:8000/${streamId}`; -``` - -### Why This Caused Failure - -1. **Icecast Default Port:** Icecast server runs on port **8000** by default -2. **icecast.xml Configuration:** The `` setting in icecast.xml defaults to 8000 -3. **Connection Refused:** FFmpeg trying to connect to port 8001 when Icecast listens on 8000 -4. **Error Output:** "connection refused" or "error number -138" - -### Impact - -- Streams could not connect to Icecast server -- All stream start attempts failed immediately -- Error messages were confusing (showed "connection refused" without clear port mismatch indication) - ---- - -## Version Status Assessment - -Based on the analysis, here's the actual status of each version: - -| Version | Functional Status | Notes | -|---------|------------------|-------| -| v1.2.0 | ✅ Working | Port 8000, stable baseline | -| v1.2.2 | ❌ Broken | Port 8001 - connection failures | -| v1.2.3 | ✅ Working | Port 8000 - port issue fixed | - -**Conclusion:** v1.2.3 is **NOT** "less functional" than v1.2.2. v1.2.3 actually **fixed** the port regression introduced in v1.2.2. - -If the user is experiencing issues with v1.2.3, those issues are: -1. Unrelated to the port regression (which was fixed) -2. Possibly related to other configuration issues -3. May be due to local environment differences - ---- - -## Recommendations - -### For Current v1.2.3 Issues - -If users are experiencing problems with v1.2.3: - -1. **Verify Icecast Port:** - ```bash - # Check icecast.xml configuration - grep -E "|" config/icecast.xml - ``` - -2. **Verify .env Configuration:** - ```bash - # Check if ICECAST_PORT is set correctly - grep ICECAST_PORT .env - ``` - -3. **Test Icecast Directly:** - ```bash - # Test if Icecast is responding - curl http://localhost:8000 - ``` - -### Code Improvements Needed - -1. **Remove Hardcoded Port:** - - Current code hardcodes `localhost:8000` - - Should read from `icecast.xml` or `.env` dynamically - -2. **Add Port Configuration Validation:** - - Warn if Icecast port doesn't match FFmpeg URL port - - Provide clear error messages on port mismatch - -3. **Enhanced Error Message (v1.2.3 already improved this):** - ```javascript - 🔧 COMMON CAUSES: - 1. Icecast not running - Start it from the dashboard - 2. Port mismatch - LANStreamer uses port 8000 by default - • Check in your icecast.xml matches 8000 - • Or set ICECAST_PORT=xxxx in your .env file - ``` - ---- - -## Files Generated - -All analysis outputs saved in `analysis_output/`: - -- `CHANGES_1.2.0_to_1.2.2_STAT.txt` - File changes statistics -- `CHANGES_1.2.2_to_1.2.3_STAT.txt` - File changes statistics -- `diff_StreamingService_*.patch` - StreamingService.js diffs -- `diff_errorDiag_*.patch` - errorDiagnostics.js comparisons (empty - didn't exist) -- `commits_*.txt` - Commit history between versions -- `all_commits.csv` - All commits with timestamps -- `package_*.json` - Package.json from each version -- `error_patterns_comparison.txt` - Error pattern analysis -- `pattern_order_comparison.txt` - Pattern order verification - ---- - -## Next Steps - -1. ✅ Root cause identified (port mismatch) -2. ✅ v1.2.3 already fixed the issue -3. Consider adding dynamic port configuration -4. Add port mismatch validation and warnings -5. Test v1.2.3 with proper Icecast configuration - ---- - -**Analysis Complete:** The regression was caused by a port change in v1.2.2 (8000→8001) and was fixed in v1.2.3 (8001→8000). diff --git a/docs/APP-FLOW.md b/docs/APP-FLOW.md new file mode 100644 index 0000000..29787d0 --- /dev/null +++ b/docs/APP-FLOW.md @@ -0,0 +1,157 @@ +# LANStreamer App Flow + +Purpose: single map of every page, user flow, and component — the source of truth for the UI redesign and for generating design prompts (Lovable/Nanobanana/Mobbin references). Last updated: 2026-10-28. + +--- + +## 1. Pages + +| Page | URL | Audience | Auth | File | +|------|-----|----------|------|------| +| Admin Login | `/login` | Operator | credentials → JWT | `public/login.html` | +| Admin Dashboard | `/dashboard` (also `/`) | Operator | localhost-only (`requireLocalhostAdmin`) | `public/index.html` | +| Listener Page | `/streams` | Anyone on LAN | none | `public/streams.html` | + +The **Electron desktop app** (packaged) opens `/dashboard` in its own window on the operator's machine; listeners always use their phone/PC browser on the LAN. Same pages either way. + +--- + +## 2. Primary flows + +### 2.1 Operator: first run + +```mermaid +flowchart TD + A[App installed / first launch] --> B{Telemetry consent prompt} + B -->|Allow| C[Telemetry ON] + B -->|Decline| D[Telemetry OFF] + C --> E[Environment check] + D --> E + E --> F{FFmpeg found?} + F -- no --> F1[Error card: install link + diagnosis] + F -- yes --> G{Icecast found?} + G -- no --> G1[Setup panel: search / custom path] + G -- yes --> H[Dashboard ready state] + H --> I[Security warning if default creds] +``` + +### 2.2 Operator: start a stream + +```mermaid +flowchart TD + A[Dashboard] --> B[Click Start New Stream] + B --> C[Add Stream modal] + C --> D[Pick audio device / file] + D --> E{Optional: Test Source
(Show test tools toggle)} + E --> F[Set name, quality, bitrate] + F --> G[Click Go Live] + G --> H{Capacity check
Icecast running?} + H -- no --> X1[Error: start Icecast first
+ quick fix] + H -- full --> X2[Error: source limit reached
+ capacity context] + H -- ok --> I[Device accessibility test] + I -- fail --> X3[Device error + diagnosis modal
(solutions, stderr, guide link)] + I -- pass --> J[FFmpeg spawn → push to Icecast] + J --> K[Stream card: LIVE badge,
uptime, listeners, stop/edit] + K --> L{Duplicate name/source?} + L -- yes --> X4[Simple duplicate modal] +``` + +### 2.3 Listener: tune in + +```mermaid +flowchart TD + A[Phone browser on LAN] --> B[Open http://LAN-IP:3001/streams] + B --> C[Event title/subtitle
+ event image or brand mark] + C --> D[Stream list S1..Sn
with LIVE state] + D --> E[Press Play on a stream] + E --> F{Stream actually running?} + F -- no --> G[Inline msg: start it on dashboard] + F -- yes --> H[Audio plays via play proxy
volume slider + mute persist] + H --> I[New stream appears? → dismissible
notification, no reload] +``` + +### 2.4 Shutdown (Electron tray mode) + +```mermaid +flowchart TD + A[Close window] --> B[Hide to tray
streams keep serving] + B --> C{User action} + C -- Show Dashboard --> D[Window restored] + C -- Stop All & Quit --> E[stopAllStreams → Icecast stop
→ tree-kill leftovers → app exit] +``` + +--- + +## 3. Dashboard layout (current) + +``` ++----------------------------------------------------------------------+ +| HEADER: logo | Icecast status pill | ffmpeg status | update bell | ++----------------------------------------------------------------------+ +| SECURITY WARNING (dismissible) — if default creds | ++----------------------------------------------------------------------+ +| STREAMS CARD | EVENT PANEL | +| toolbar: [Show test tools][Refresh][Start/Stop All] | (image, | +| stream rows: S1..Sn | title, | +| [LIVE badge] name | uptime | listeners | about) | +| [play/stop][edit][reorder ↑↓][delete] +---------------+ +| empty state: Quick Start guide (dismissible) | CONTACT & | +| | FEEDBACK | +| ICECAST CARD: start/stop, mounts, capacity | (collapsed) | ++----------------------------------------------------------------------+ +| FOOTER: version | GitHub link | ++----------------------------------------------------------------------+ +``` + +## 4. Listener layout (current) + +``` ++----------------------------------------------+ +| HEADER: logo | Listen to Streams link | ++----------------------------------------------+ +| Event title + subtitle | ++----------------------+-----------------------+ +| STREAM CARDS | EVENT IMAGE / | +| S1 [LIVE] name | brand mark default | +| [▶ Play] +-----------------------+ +| per-card: volume, | ABOUT (default text) | +| mute +-----------------------+ +| S2 ... | CONTACT & FEEDBACK | +| | (collapsed default) | ++----------------------+-----------------------+ +| FOOTER: version | GitHub | license | ++----------------------------------------------+ +``` + +**Redesign note (Mobbin + Stemtracks direction):** listener page should get a sticky bottom mini-player (Sonos/Spotify pattern) so controls survive scrolling; one dominant Play per card; LIVE = color + text, never dot-only. + +--- + +## 5. Component map + +| Component | File | Used by | Notes | +|-----------|------|---------|-------| +| FFmpegStreamsManager | `components/FFmpegStreamsManager.js` | Dashboard | stream CRUD, start/stop/reorder, modals, Test Source, Quick Start | +| IcecastManager | `components/IcecastManager.js` | Dashboard | start/stop, mounts, capacity, install search | +| EventManager | `components/EventManager.js` | Dashboard | event title/subtitle/about/image (persisted `config/`) | +| ContactManager | `components/ContactManager.js` | Dashboard | WhatsApp/phone/email shown on listener page | +| HeaderComponent | `components/HeaderComponent.js` | Both | logo, status pills, update bell | +| FooterComponent | `components/FooterComponent.js` | Both | version, links | +| UpdateNotification | `components/UpdateNotification.js` | Dashboard | GitHub release check | +| LobbyMusicPlayer | `components/LobbyMusicPlayer.js` | Dashboard | file-playback preview | +| FileBrowser | `components/FileBrowser.js` | Dashboard | pick input file | +| ComponentManager | `components/ComponentManager.js` | Dashboard | lifecycle orchestration | +| LSNotices | `assets/js/notice-dismiss.js` | Both | dismissible notices w/ Show stubs | +| StreamListener (inline) | `streams.html` | Listener | play proxy, volume, new-stream notif | + +## 6. Backend services (context for redesigners) + +| Service | Role | +|---------|------| +| `StreamingService` | FFmpeg spawn/lifecycle, formats (MP3/AAC/OGG), persistence, capacity | +| `IcecastService` | install detection, start/stop, stats (localhost polls), hostname auto-sync, security check | +| `AudioDeviceService` | device discovery (dshow; wasapi fallback) | +| `UpdateService` | GitHub release polling (to be replaced by electron-updater) | +| `TelemetryService` | PostHog, opt-in, anonymous ID, queue-and-flush | + +API surface used by the UI: `/api/streams/*` (status, start, stop, start-all, stop-all, reorder, play/:id proxy), `/api/system/*` (config, icecast start/stop/status, audio-devices, security-check), `/api/settings/*`, `/api/contact-details`, `/api/auth/login`. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 017dccb..c63a2b6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,15 +5,25 @@ All notable changes to LANStreamer will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - -### Planned -- Stream configuration and mount point management -- FFmpeg integration for audio streaming -- Listener web player interface -- QR code generation for listener access -- Real-time stream monitoring and statistics -- Final packaging as standalone `.exe` +## [2.0.0] — 2026-11-01 (first packaged release) + +Everything below ships in the Windows installer and standalone builds. For the release process, see [GO-LIVE-CHECKLIST.md](./GO-LIVE-CHECKLIST.md). + +### Added — the Electron desktop app +- Full Electron shell: dashboard window, tray icon (hide-to-tray, Stop All & Quit), single-instance lock, first-run telemetry notice (ON by default, one-click off), graceful shutdown that stops streams + Icecast +- **Bundled binaries:** the installer ships FFmpeg + the Icecast kit (extraResources), with runtime resolution preferring the bundle over system installs — a fresh machine needs NOTHING pre-installed. Bundled Icecast runs from a writable per-user config/log dir (no more Program Files EPERM), and all mutable state (streams, device config, contact details, uploads, telemetry consent) lives in `%APPDATA%/LANStreamer` on packaged installs +- Admin authentication: login page, JWT session (7-day), localhost lock so admin APIs/dashboard refuse non-localhost connections; default-credentials banner with change instructions; logout +- **PostHog telemetry (ON by default, opt-out — owner decision 2026-11-01):** server events (app start, stream started/stopped/failed with error category) and **listener failure reports** (playback error with media code + browser, buffer stalls, off-air grace/give-up, page-load failures) relayed through the operator's server; distributed installs get telemetry via a bundled build key (asar path fix); first run shows an informational notice with one-click Turn-off +- Event banner system: operator-uploaded artwork with lightbox, bundled default banner when none set +- In-app guide page (`/docs`), QR code sharing, share card that tracks the PC's current LAN address + +### Changed — 2026 redesign +- Complete UI redesign (operator dashboard + listener page): token-based design system, status strip, grouped stream rows with pinned-right row actions, side drawer for stream creation, listener hero + channel picker with visible play affordances, sticky opaque headers, flat surfaces (no card shadows/hover/animations), full light/dark themes, fully offline (all fonts/assets vendored) +- Process management hardened: Windows process-tree kills in the right order (no more orphaned FFmpeg holding audio devices), icecast self-healing on stream start/restart, stale LAN IP detection when the network changes +- **Low-latency playback (2026-10-30):** listener audio delay cut from a reported 4–5 s to ~1 s class — MSE-based player keeps ~0.5 s buffered ahead (vs ~2.3 s browser default, measured) and snaps back to the live edge after WiFi jitter or pauses; FFmpeg capture runs with low-delay flags (`nobuffer`, `low_delay`, `probesize 32k`, `analyzeduration 0`, `flush_packets 1`); the play proxy disables Nagle and flushes headers; bundled Icecast uses a 16 KB join burst. Safari/iOS automatically use the classic direct-stream path + +### Known gaps +- macOS/Linux support is source-only --- diff --git a/docs/GO-LIVE-CHECKLIST.md b/docs/GO-LIVE-CHECKLIST.md new file mode 100644 index 0000000..43c165a --- /dev/null +++ b/docs/GO-LIVE-CHECKLIST.md @@ -0,0 +1,126 @@ +# 🚀 Go-Live Checklist — before bundling & shipping LANStreamer + +Working document for the first packaged release of the **Electron desktop app**. Version: **2.0.0** (bumped in package.json 2026-10-31 — Jerry's call: the redesign + desktop-app transformation is a MAJOR release; the old `1.2.4 → 1.3.0` minor target is superseded). Work through every section; tick items only with evidence (test run, screenshot, command output). + +> Run the automated suite first — it clears most regressions in minutes: +> `npm test` → then `npm run electron:smoke` → then the verify scripts in [README-DEVELOPERS.md](./README-DEVELOPERS.md) against a fresh server. + +--- + +## 0. Security must-fixes (external review 2026-10-29 — block shipping until done) + +1. ~~**Bind bundled Icecast to loopback + random per-install passwords.**~~ **SHIPPED (Jerry's final decision, 2026-10-30 — supersedes the earlier same-day "ship stock" call): public software shipping known-default Icecast credentials is hackable by anyone who knows the app.** The generated bundled config now binds `127.0.0.1` (listeners receive audio through the :3001 play proxy — never Icecast directly) and randomizes source/relay/admin passwords per install (`ensureVendoredIcecastConfig`; the app parses them itself, nobody types them). Unit-tested (`vendorPaths.test.js` hardening contract) + live-verified. OWN system Icecast installs keep their own config — the dashboard warns while stock credentials are active there. +2. **Binary provenance:** pin SHA256 for the FFmpeg download (fail build on mismatch); replace "copy Icecast kit from maintainer's machine" with a pinned official-kit download + hash; switch to the gyan **essentials** build (clean GPL redistribution, ~48 MB). **Status:** FFmpeg essentials + pinned SHA256 SHIPPED (`src/utils/binaries.js`); Icecast kit still copied from the validated local 2.5.0 portable — pinned official-kit download remains open. +3. **Icecast 2.4.4 is end-of-support 2026-12-31** (and has CVE history) — bundle 2.5.x. **SHIPPED:** vendor kit is 2.5.0. +4. ~~**Packaged installs cannot use `.env`** — the "change admin credentials" path is impossible there.~~ **RESOLVED (2026-10-30, final policy): documented defaults + prompt to change + in-app change + documented recovery.** The *Admin account* card changes the login in-app (scrypt-hashed in the data dir, JWT secret rotates). Random per-install admin minting was tried and REVERTED the same day as overkill — the localhost lock already keeps the LAN out of the admin panel, and the operator must stay in full control. Forgot-password recovery: delete `security.json` (path shown on the localhost-only login page + README + /docs) → documented defaults return → change again in-app. E2E: `scripts/verify-admin-creds.mjs`. +5. **GPL artifacts inside the installer:** ship FFmpeg/Icecast license texts + source-offer, not just release-note credit. **SHIPPED:** `vendor/licenses/` (GPL-2.0/3.0 texts + source-offer README) rides extraResources. +6. Ship **SHA256SUMS** alongside release artifacts (cheap unsigned-installer mitigation). + +--- + +## 1. Clean-slate functional pass (on the dev machine) + +Start from a pristine state — most "bugs" this project has hit were dirty-state artifacts (leftover servers, stale config, cached tabs). + +- [ ] Kill every LANStreamer instance first: `netstat -ano | findstr :3001 :3002 :8200` → nothing listening; `tasklist | findstr /i "ffmpeg icecast"` → nothing running +- [ ] Fresh app launch (`npm run electron:dev`): dashboard window opens, boot screen resolves, version label shows +- [ ] Login: wrong password rejected; `admin` / `lanstreamer123` accepted; logout works; token survives app restart (7-day session) +- [ ] First-run banner shows while defaults are active; disappears after changing credentials via `.env` +- [ ] Stream lifecycle: **Add stream** (Save, stays stopped) → **Go live** → status LIVE + uptime ticking → **Stop** → **Retry** → edit (change input) → duplicate → reorder (Move up/down) → delete. No button ever hangs (all mutating calls have timeouts) +- [ ] Start All / Stop All with 2+ streams +- [ ] Every stop/quit leaves **zero orphaned `ffmpeg.exe`** in Task Manager (this was the zombie-device bug — verify after every stop path, including edit-while-live and quit-while-live) +- [ ] **Crash-path orphan test:** Task Manager → End task on LANStreamer mid-stream → relaunch → stale children are cleaned. *(Startup sweep SHIPPED: Electron main kills orphaned ffmpeg/icecast matched by vendored executable path — never by process name. This item now just verifies it works.)* +- [x] **Sleep/hibernate mid-broadcast:** SHIPPED — `powerSaveBlocker` (prevent-app-suspension) engages while any stream is live and releases when none are; on resume, streams that were live before suspend are auto-restored after a 5s device-settle delay +- [ ] **Non-ASCII paths:** test with a Windows username containing spaces + non-ASCII (e.g. `José Müller`) and a Unicode dshow device name — spawn quoting and icecast config paths are classic failure points +- [ ] Error path: start a stream on a device already held by another app → friendly busy error with **Change input** / **Retry**, full diagnostic collapsed behind a link to /docs +- [ ] Audio inputs card (under the streams card): collapsed by default, opens/closes, rescan works without toggling the card, IN USE / IDLE / MISSING statuses correct +- [ ] Event details + feedback contact cards save and show on the listener page; banner upload → cover display → lightbox; remove image → bundled default banner appears +- [ ] Icecast info card shows running status, listeners count, uptime + +## 2. Listener experience (real devices, not just devtools) + +- [ ] Open `http://:3001/streams` on **Android Chrome, iOS Safari, and a desktop browser** +- [ ] **iOS specifics:** audio continues on lock screen? Silent-switch behavior? MediaSession/now-playing metadata (nice-to-have) +- [ ] Channel rows show LIVE chips, uptime, bitrate; visible play circles; icon flips play ⇄ pause correctly on every tap (pause→play flip was a real bug — test the cycle) +- [ ] Switching channels mid-play; big player reflects the change; mini-player on mobile while scrolled +- [ ] Kill the stream on the dashboard while a listener is playing → listener sees "went off air — waiting…", grace countdown, then "off air, will resume"; restart the stream → auto-resumes without user action +- [ ] Pause a listener → it never auto-resumes until they press play +- [ ] QR code: scan with a phone camera → opens the listener page directly +- [ ] Both themes (dark/light) on every listener surface — dark-on-dark / light-on-light contrast bugs are this project's most-reported class +- [ ] 3+ simultaneous listeners for 15+ minutes: audio stays in sync, no memory creep in Task Manager (node + ffmpeg) + +## 3. Security pre-flight + +- [ ] Change the default admin credentials (`ADMIN_USERNAME` / `ADMIN_PASSWORD` / `JWT_SECRET` in `.env`) and confirm the banner retires +- [ ] From a **different machine** on the LAN: `/streams` loads (public) but `/dashboard` and admin APIs are refused (localhost lock) — `curl http://:3001/dashboard` from another device must NOT render the dashboard +- [ ] Listener page never displays credentials anywhere +- [ ] `git status` clean of secrets; `electron/telemetry-key.json` and `.env` are gitignored (verify: `git check-ignore electron/telemetry-key.json .env`) +- [ ] Rate-limited telemetry endpoint sane: `for i in {1..35}; do curl -s -o /dev/null -w "%{http_code} " -X POST http://localhost:3001/api/telemetry/client-event -H "Content-Type: application/json" -d "{\"event\":\"playback_stalled\"}"; done` → ends in 429 + +## 4. Telemetry (PostHog — eu.posthog.com) + +- [ ] `node scripts/prepare-telemetry-key.mjs` reports "Injected for packaged build" (bundles the key from the credential store; file gitignored) +- [ ] With telemetry ON (tray toggle): events visible in PostHog Live within ~30s — `lanstreamer_app_start`, `stream_started` / `stream_failed` when you exercise streams +- [ ] Trigger a listener playback failure (or run `scripts/verify-client-telemetry.mjs`) → `lanstreamer_client_*` events arrive with `surface=listener` +- [ ] With telemetry OFF (tray toggle): no events arrive at all; app behaves identically (telemetry must never affect function) +- [ ] README "Telemetry & Privacy" matches what's actually collected (it does as of this writing — re-read after any telemetry change) + +## 5. Resilience & environment + +- [ ] Quit app mid-stream (tray → Stop All & Quit) → zero orphaned processes; relaunch → auto-start streams restore (if `autoStart` set) +- [ ] Close the window (not quit) → app hides to tray, streams keep serving; listeners confirm audio continues +- [ ] Change network (different subnet / hotspot): share card + QR update to the new LAN IP; old links fail gracefully +- [ ] Port collision: start a second instance → it must not corrupt the first (single-instance lock) and the "port in use" guidance appears in /docs +- [ ] Windows Firewall prompt on first run: accept for **Private networks**; listener on another device confirms reachability +- [ ] **Public-profile gotcha:** café/conference/hotspot networks default to the Public profile → inbound 3001 silently blocked. Verify reachability on a hotspot; document "set network profile to Private" for operators +- [ ] **AV/MOTW pass:** clean-VM test with Defender real-time protection ON — unsigned vendored exes spawning from `resources\` are false-positive territory; verify the downloaded-from-GitHub flow end-to-end (Mark-of-the-Web can block child exes from extracted archives) +- [ ] Offline check: `node scripts/verify-offline.mjs` — app fully renders with all non-localhost requests blocked (fonts/assets vendored) +- [ ] Reboot the operator PC with the app configured: relaunch and confirm streams/config persist (config/ survives) + +## 6. Packaging (NSIS installer + portable — bundles FFmpeg + Icecast) + +The installer carries everything: `electron:build:win` auto-runs `fetch-vendor-binaries.mjs` (fills gitignored `vendor/` with FFmpeg + the Icecast kit and a **clean stock** icecast.xml — never the local machine's passwords) then `prepare-telemetry-key.mjs`, then electron-builder packs `vendor/` as extraResources. At runtime the app prefers the bundled binaries (explicit `FFMPEG_PATH` / `ICECAST_EXE_PATH` env still wins) and keeps all mutable state in `%APPDATA%/LANStreamer` — nothing writes next to the exe. + +- [ ] `npm test` green immediately before building +- [ ] `npm run electron:build:win` → installer + portable in `release/`; installer size sane (target ~100 MB once the bundle switches FULL → essentials ffmpeg and drops ffplay/ffprobe; ~230 MB with today's FULL build) +- [x] **Binary strategy — SHIPPED (2026-10-30):** first-run functional probe (dshow devices / encoder chain / 2 s end-to-end against a throwaway loopback icecast; ~3.7 s measured, non-blocking) with the result pinned to `binary-probe.json` and re-run only on stale pins; resolution order env → vendored → probe-passed system → download prompt; `GET /api/system/binary-probe` for support. Essentials FFmpeg verified on the maintainer's real machine (all stages pass, real stream, listener playback). Remaining: the download-on-demand UI for the (rare) probe-failure tier. +- [x] Electron locales trimmed to en-US (`electronLanguages`) — shipped 07f2d6e +- [x] Default event banner recompressed (1.27 MB → 188 KB, 2400 px) — shipped 07f2d6e +- [ ] **Install on a clean Windows machine (or VM) with NOTHING pre-installed** — no Node, no FFmpeg, no Icecast. This is the single most important check ("works on my PC" insurance). On that machine: + - [ ] App launches, dashboard opens, login works, version shows correctly (from package.json, not "unknown") + - [ ] First-run telemetry consent dialog appears (bundled key resolves) — test BOTH Allow and Decline paths + - [ ] Create a stream and go live **using only the bundled binaries**; a phone plays it (check Task Manager: ffmpeg.exe and icecast.exe run from the install's `resources\vendor\`) + - [ ] All config/uploads land in `%APPDATA%/LANStreamer` (delete + reinstall the app → streams and settings survive) +- [ ] Confirm the build does NOT contain `.env`, the dev machine's `config/`, `public/uploads/`, or a real-credential icecast.xml (inspect the installer's resources) +- [ ] SmartScreen will warn on an unsigned exe — decide: accept for now (document in release notes: "More info → Run anyway") or invest in code signing later +- [ ] Portable build: runs from a folder without installing; config lands somewhere sensible and persists between runs (decide: true-portable via `PORTABLE_EXECUTABLE_DIR` vs `%APPDATA%`, and document) +- [ ] **Upgrade path:** build two dummy versions, NSIS over-install test (old → new: settings survive, no duplicate entries) +- [ ] **Uninstall story:** what uninstall leaves behind (`%APPDATA%/LANStreamer` stays — intentional? document) +- [ ] **Source-install migration:** existing source/ZIP users have config in `./config/` — either migrate to `%APPDATA%` on first packaged run or document "not migrated, set streams up again" +- [ ] **Build the release artifact the way CI would** (or add CI packaging) — don't ship from an environment CI never exercised +- [ ] Release notes must credit the bundled GPL binaries (FFmpeg gyan.dev build, Icecast) with their licenses + +## 7. Release: version, tag, notes on GitHub + +- [ ] Bump `version` in **package.json** (already bumped to 2.0.0 — verify the installer filename says 2.0.0) +- [ ] Move `[Unreleased]` in [CHANGELOG.md](./CHANGELOG.md) to `## [2.0.0] - `; write the user-facing highlights (Electron app, admin login, redesign, listener page, telemetry opt-in, default event banner) +- [ ] Update README if anything user-facing changed (credentials section, download link once releases exist) +- [ ] Merge `dev` → `main` via PR (never push straight to main); CI green on the PR +- [ ] Tag and push: `git tag v2.0.0 && git push origin v2.0.0` +- [ ] Create the GitHub release with the installers attached and real release notes: + ```bash + gh release create v2.0.0 release/*.exe release/*.yml SHA256SUMS.txt \ + --title "LANStreamer v2.0.0 — desktop app" \ + --notes-file release-notes-v2.0.0.md + ``` +- [ ] Generate and attach **SHA256SUMS.txt** for all release artifacts (partial SmartScreen mitigation for an unsigned installer) +- [ ] Release notes template (user-facing, non-technical): **What's new** (5–8 bullets) · **Installing** (nothing to pre-install — FFmpeg + Icecast come bundled; SmartScreen note) · **Upgrading from a source/ZIP install** · **Known issues** · **Telemetry disclosure** (opt-in, what's collected, how to decline; Decline must persist across app updates) · **Licenses** (bundled FFmpeg/Icecast are GPL — offer in notes, texts inside the installer) +- [ ] Smoke the release as a stranger would: fresh VM, download the installer **from the GitHub release page**, install, run one stream end-to-end +- [ ] Post-release: watch PostHog for `app_start` from install IDs that aren't yours; triage `stream_failed` / `lanstreamer_client_*` events as user reports come in + +## 8. Deferred (explicitly NOT blocking this release) + +- Code signing certificate (SmartScreen reputation builds over time; signing is a follow-up) +- macOS/Linux packaging (source-only for now) +- Auto-update via electron-updater (update checks point at GitHub releases today) +- PWA for listeners (deliberately removed — DHCP IP changes break it; see docs) diff --git a/docs/LATENCY.md b/docs/LATENCY.md new file mode 100644 index 0000000..c3f828a --- /dev/null +++ b/docs/LATENCY.md @@ -0,0 +1,57 @@ +# Latency in LANStreamer + +What affects the delay between the operator's audio and what listeners hear, and what can be done about it — split into what **we control** (app side) and what **the operator/event setup controls** (their side). Research date: 2026-10-28. References at the end. + +## Where latency comes from (total = sum of all stages) + +```mermaid +flowchart LR + A[Audio device capture] --> B[FFmpeg encode buffer] + B --> C[Icecast server queue + burst/prebuffer] + C --> D[Network transfer LAN] + D --> E[Browser player buffer] + E --> F[Listener hears it] +``` + +Encoder buffer + server queue + burst/prebuffer + player buffer dominate; the LAN network itself is usually < 5 ms. + +## App side (what LANStreamer can do) + +**Status 2026-10-30: the low-latency round SHIPPED.** Measured on a live stream (Chromium, `scripts/verify-latency.mjs`): buffered-ahead in the player dropped from **~2.3 s (browser default) to ~0.5 s steady**, time-to-first-audio ~1.6 s after the tap. Architecture floor with MP3-over-HTTP is ~0.8–1.2 s end-to-end; going below that means changing transport (WebSocket/WebRTC + Opus), not tuning. + +| Lever | Status (2026-10-30) | Trade-off | +|---|---|---| +| **Listener player (the big lever)** | ✅ SHIPPED — MSE-based playback (`public/assets/js/lowlatency.js`): the page fetches the play-proxy stream itself and appends ~0.5 s chunks to a MediaSource buffer, snapping the playhead back to the live edge whenever drift exceeds ~0.65 s (rate-limited to 1/s). Safari/iOS (no MP3-in-MSE) and any MSE failure fall back to the plain `