Skip to content

ACTUALLY WORKING REACT - #18

Draft
snoopythe3-from-scratch wants to merge 1 commit into
mainfrom
react
Draft

ACTUALLY WORKING REACT#18
snoopythe3-from-scratch wants to merge 1 commit into
mainfrom
react

Conversation

@snoopythe3-from-scratch

@snoopythe3-from-scratch snoopythe3-from-scratch commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added a React-based application foundation with Vite.
    • Added a starter interface featuring React and Vite branding, plus an interactive counter.
    • Added development, build, preview, and lint workflows.
  • Documentation

    • Added setup and usage guidance for the React/Vite template.
    • Added security vulnerability reporting guidance.
  • Chores

    • Added project configuration and ignore rules for build artifacts, logs, local files, and editor metadata.

@vercel

vercel Bot commented Feb 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
plexstreamlive Error Error Feb 27, 2026 6:05pm

@netlify

netlify Bot commented Feb 27, 2026

Copy link
Copy Markdown

Deploy Preview for plexstream ready!

Name Link
🔨 Latest commit 63f8b20
🔍 Latest deploy log https://app.netlify.com/projects/plexstream/deploys/69a1dcfc99b87600079e697c
😎 Deploy Preview https://deploy-preview-18--plexstream.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch react

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (3)
eslint.config.js (1)

16-23: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Remove the conflicting ECMAScript version declarations.

languageOptions.ecmaVersion is 2020, while nested parserOptions.ecmaVersion is latest. Keep one intentional target so parsing and lint rules use the same syntax baseline.

src/index.css (1)

6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve the reported Stylelint violations.

Stylelint flags the empty lines before declarations at Lines 6 and 10 and the casing of optimizeLegibility at Line 11. Fix these or verify that the external Stylelint check is not intended to gate this project.

Source: Linters/SAST tools

old-stuff/styles/style.css (1)

5-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop quotes around "Roboto" per stylelint, and consider deduplicating h2/h3.

Stylelint's font-family-name-quotes flags the quoted font name on lines 6 and 16; h2/h3 are otherwise identical and could share a selector.

🎨 Suggested fix
-h2 {
-  font-family: "Roboto", sans-serif;
+h2, h3 {
+  font-family: Roboto, sans-serif;
   font-optical-sizing: auto;
   font-weight: 300;
   font-style: normal;
   font-variation-settings:
     "wdth" 100;
   color: white;
 }
-
-h3 {
-  font-family: "Roboto", sans-serif;
-  font-optical-sizing: auto;
-  font-weight: 300;
-  font-style: normal;
-  font-variation-settings:
-    "wdth" 100;
-  color: white;
-}

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 76242e88-874b-49cb-ab89-deeac115490f

📥 Commits

Reviewing files that changed from the base of the PR and between 1f57b77 and 63f8b20.

⛔ Files ignored due to path filters (4)
  • old-stuff/favicon.ico is excluded by !**/*.ico
  • package-lock.json is excluded by !**/package-lock.json
  • public/vite.svg is excluded by !**/*.svg
  • src/assets/react.svg is excluded by !**/*.svg
📒 Files selected for processing (16)
  • .gitignore
  • README.md
  • eslint.config.js
  • index.html
  • old-stuff/SECURITY.md
  • old-stuff/index.html
  • old-stuff/server.js
  • old-stuff/signup.html
  • old-stuff/styles/style.css
  • old-stuff/templates/multistream.html
  • package.json
  • src/App.css
  • src/App.jsx
  • src/index.css
  • src/main.jsx
  • vite.config.js

Comment thread eslint.config.js
Comment on lines +10 to +24
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Give the archived Node server its own ESLint environment.

npm run lint executes eslint ., but this configuration matches all *.js/*.jsx files and provides only browser globals. The archived old-stuff/server.js is a Node/WebSocket server, so Node globals can be reported as undefined and make lint fail. Scope this config to React sources and add a Node override for old-stuff/**/*.js, or explicitly ignore the archive.

Proposed configuration shape
-    files: ['**/*.{js,jsx}'],
+    files: ['src/**/*.{js,jsx}'],
...
+  {
+    files: ['old-stuff/**/*.js'],
+    languageOptions: {
+      globals: globals.node,
+    },
+  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
files: ['src/**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
},
{
files: ['old-stuff/**/*.js'],
languageOptions: {
globals: globals.node,
},
},

Comment thread old-stuff/index.html
Comment on lines +139 to +459
<script>
/* kept behavior intact and fixed start/stop/search without changing UI layout */

let currentUser = null;
const userColors = {};
const config = { iceServers:[{ urls:'stun:stun.l.google.com:19302' }] };
let ws = new WebSocket('ws://localhost:8080');
let peerConnection = null;
let broadcasterPC = null;
let localStream = null;

/* small util */
function randomColor(){
const colors=["#1e90ff","#ff4c4c","#4cff4c","#ffd93d","#a259ff","#ff7f50","#00ced1"];
return colors[Math.floor(Math.random()*colors.length)];
}

/* NAV */
function showSection(id){
document.querySelectorAll('main section').forEach(s=>s.classList.add('hidden'));
document.getElementById(id).classList.remove('hidden');
}

/* LOGIN */
function showLogin(){ document.getElementById('login-modal').classList.remove('hidden'); }
function hideLogin(){ document.getElementById('login-modal').classList.add('hidden'); }

function login(){
const u = document.getElementById('login-user').value.trim();
const p = document.getElementById('login-pass').value.trim();
if(!u || !p) return alert('fill both fields');
currentUser = u;
if(!userColors[u]) userColors[u] = randomColor();
hideLogin();
document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`;
alert('Logged in as '+u);
}
function signup(){ login(); }
function logout(){
currentUser = null;
document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`;
alert('Logged out');
}

/* Go Live button logic */
document.getElementById('go-live-btn').addEventListener('click', ()=>{
if(!currentUser) return alert('Login first');
showSection('dashboard');
});

/* CHAT rendering */
const chatContainer = document.getElementById('chat-container');
const messagesDiv = document.getElementById('messages');

function renderChatLine(user, text){
const p = document.createElement('p');
p.className = 'chat-msg';
const strong = document.createElement('strong');
strong.textContent = user;
strong.style.color = userColors[user] || '#fff';
const span = document.createElement('span');
span.textContent = ': ' + text;
p.appendChild(strong);
p.appendChild(span);
// append to both viewer chat and dashboard messages if present
if(chatContainer) chatContainer.appendChild(p.cloneNode(true));
if(messagesDiv) messagesDiv.appendChild(p.cloneNode(true));
if(chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
if(messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

/* sending chat */
document.getElementById('chat-send-btn').addEventListener('click', sendMessage);
function sendMessage(){
const input = document.getElementById('chat-input');
const msg = input.value.trim();
if(!currentUser) return alert('Login first');
if(!msg) return;
ws.send(JSON.stringify({ type:'chat', chat:msg, user:currentUser }));
renderChatLine(currentUser, msg);
input.value = '';
}
document.getElementById('chat-input').addEventListener('keypress', e=>{ if(e.key==='Enter') sendMessage(); });

function sendMessageDashboard(){
const input = document.getElementById('chatInputDashboard');
const msg = input.value.trim();
if(!currentUser) return alert('Login first');
if(!msg) return;
ws.send(JSON.stringify({ type:'chat', chat:msg, user:currentUser }));
renderChatLine(currentUser, msg);
input.value = '';
}
document.getElementById('chatInputDashboard').addEventListener('keypress', e=>{ if(e.key==='Enter') sendMessageDashboard(); });

/* START / STOP streaming logic fixed and intact */
const startBtn = document.getElementById('start-stream-btn');
const stopBtn = document.getElementById('stop-stream-btn');
const broadcasterVideo = document.getElementById('broadcasterVideo');

startBtn.addEventListener('click', async ()=>{
if(!currentUser) return alert('Login first');
startBtn.classList.add('hidden');
stopBtn.classList.remove('hidden');

// get camera
try{
localStream = await navigator.mediaDevices.getUserMedia({ video:true, audio:true });
broadcasterVideo.srcObject = localStream;

// create PeerConnection for broadcaster (it will send offer to others via ws)
broadcasterPC = new RTCPeerConnection(config);
localStream.getTracks().forEach(t=>broadcasterPC.addTrack(t, localStream));

broadcasterPC.onicecandidate = ({candidate})=>{
if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate, forStreamOwner: true }));
};

const offer = await broadcasterPC.createOffer();
await broadcasterPC.setLocalDescription(offer);

// tell server about offer and new stream
const title = document.getElementById('stream-title').value || 'Untitled';
const category = document.getElementById('stream-category').value || 'chat';
ws.send(JSON.stringify({ type:'offer', offer }));
ws.send(JSON.stringify({ type:'newStream', title, category, user: currentUser }));

// optional show stream key placeholder unchanged
}catch(err){
console.error('start stream failed', err);
alert('could not start stream check camera permissions');
startBtn.classList.remove('hidden');
stopBtn.classList.add('hidden');
}
});

stopBtn.addEventListener('click', ()=>{
stopBtn.classList.add('hidden');
startBtn.classList.remove('hidden');
// notify server
ws.send(JSON.stringify({ type:'stopStream' }));

// close pc and tracks
try{
if(broadcasterPC){
broadcasterPC.getSenders().forEach(s=>broadcasterPC.removeTrack(s));
broadcasterPC.close();
broadcasterPC = null;
}
if(localStream){
localStream.getTracks().forEach(t=>t.stop());
broadcasterVideo.srcObject = null;
localStream = null;
}
}catch(e){ console.warn(e); }
});

/* COPY KEY */
function copyKey(){ navigator.clipboard.writeText(document.getElementById('stream-key').innerText); alert('Copied!'); }

/* SEARCH */
function searchStreams(){
const q = document.getElementById('search-input').value.trim();
ws.send(JSON.stringify({ type:'search', query: q }));
}
function renderResults(list){
const container = document.getElementById('search-results');
container.innerHTML = '';
if(!list || list.length === 0){
// keep same UI feel just show empty
return;
}
list.forEach(s=>{
const card = document.createElement('div');
card.className = 'card';
// show only title as requested and keep UI unchanged
card.textContent = s.title + (s.live ? ' (LIVE)' : '');
card.addEventListener('click', ()=> {
// show viewer section and send request to watch that stream
document.getElementById('viewer-title').textContent = s.title;
document.getElementById('stream-id').textContent = s.id || '';
showSection('viewer');

// prepare to receive offer from owner if server will forward it
// create peerConnection to receive remote track
if(peerConnection){
try{ peerConnection.close(); } catch(e){}
peerConnection = null;
}
peerConnection = new RTCPeerConnection(config);
peerConnection.ontrack = (e) => {
document.getElementById('stream-player').srcObject = e.streams[0];
};
peerConnection.onicecandidate = ({candidate})=>{
if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate, watchingStreamId: s.id }));
};

// tell server we want to watch this stream will trigger server to forward offer or instruct owner
ws.send(JSON.stringify({ type:'watchStream', id: s.id }));
});
container.appendChild(card);
});
}

/* WEBSOCKET message handling */
ws.addEventListener('open', ()=>console.log('ws open'));
ws.addEventListener('error', (e)=>console.error('ws err', e));

ws.addEventListener('message', async ev=>{
try{
const msg = JSON.parse(ev.data);

// chat
if(msg.type === 'chat'){
// server broadcasts chat as {type:'chat', user, chat}
renderChatLine(msg.user, msg.chat);
}

// streams update broadcast maybe used to autofill featured or results
else if(msg.type === 'streamsUpdate'){
// server sends full streams array
// we keep UI intact so only render to search results area when a query was made by server
// but to keep user convenience update featured streams quickly minimal update
// we do not alter layout just update featured list small
const featured = document.getElementById('featured-streams');
featured.innerHTML = '';
msg.streams.slice(0,6).forEach(s=>{
const c = document.createElement('div');
c.className = 'card';
c.textContent = s.title;
featured.appendChild(c);
});
}

// search results
else if(msg.type === 'searchResults'){
renderResults(msg.results || []);
}

// WebRTC offer arrives from stream owner forwarded by server
else if(msg.type === 'offer' || msg.offer){
const offerMsg = msg.offer || msg;
// viewer side answer flow
if(peerConnection === null){
peerConnection = new RTCPeerConnection(config);
peerConnection.ontrack = (e)=>document.getElementById('stream-player').srcObject = e.streams[0];
peerConnection.onicecandidate = ({candidate})=>{
if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate }));
};
}
await peerConnection.setRemoteDescription(new RTCSessionDescription(offerMsg.offer || offerMsg));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
ws.send(JSON.stringify({ type:'answer', answer }));
}

// answer for broadcaster
else if(msg.type === 'answer' || msg.answer){
const answerMsg = msg.answer || msg;
if(broadcasterPC){
await broadcasterPC.setRemoteDescription(new RTCSessionDescription(answerMsg.answer || answerMsg));
} else if(peerConnection){
await peerConnection.setRemoteDescription(new RTCSessionDescription(answerMsg.answer || answerMsg));
}
}

// ice candidates
else if(msg.type === 'iceCandidate' || msg.iceCandidate){
const cand = msg.iceCandidate || msg;
try{
if(peerConnection) await peerConnection.addIceCandidate(new RTCIceCandidate(cand));
if(broadcasterPC) await broadcasterPC.addIceCandidate(new RTCIceCandidate(cand));
}catch(e){ console.warn('ice add err', e); }
}

}catch(e){
console.error('ws msg parse err', e);
}
});

/* render chat lines when user sends or incoming handled above */
function renderChatLine(user, text){
const p = document.createElement('p');
p.textContent = user + ': ' + text;
// append to both viewer and dashboard
const viewerChat = document.getElementById('chat-container');
const dashChat = document.getElementById('messages');
if(viewerChat) viewerChat.appendChild(p.cloneNode(true));
if(dashChat) dashChat.appendChild(p.cloneNode(true));
}

/* keep old dummy featured example so UI not empty */
(function seedDummy(){
const f = document.getElementById('featured-streams');
if(f) {
if(f.children.length === 0){
const a = document.createElement('div'); a.className='card'; a.textContent='Stream 1'; f.appendChild(a);
const b = document.createElement('div'); b.className='card'; b.textContent='Stream 2'; f.appendChild(b);
const c = document.createElement('div'); c.className='card'; c.textContent='Stream 3'; f.appendChild(c);
}
}
})();

<script type="module">
// Import the functions you need from the SDKs you need
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.5.0/firebase-app.js";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyAyiiWVeRjxSsxUE8fR1DyCPEZwt7o3iHc",
authDomain: "plexstream-ef9b4.firebaseapp.com",
projectId: "plexstream-ef9b4",
storageBucket: "plexstream-ef9b4.firebasestorage.app",
messagingSenderId: "130967153299",
appId: "1:130967153299:web:e424baeb43541f1401bb66"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
</script>
</script>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Malformed nested <script> tags likely break all page JavaScript.

The <script> opened at line 139 is never closed before the second <script type="module"> opens at line 442. Per HTML parsing rules, the tokenizer only ends script content on the literal </script> sequence, so everything from line 140 through line 458 (including the literal text <script type="module"> and the import statement) becomes the text content of the first script. A bare <script ...> string and a top-level import are both syntax errors in a non-module script, so this will throw at parse time and prevent login, chat, streaming, and search from ever running. The stray </script> on line 459 (flagged by HTMLHint as unpaired) confirms the tag nesting is broken.

🐛 Suggested fix
     }
 })();
+</script>
 
 <script type="module">
   // Import the functions you need from the SDKs you need
   import { initializeApp } from "https://www.gstatic.com/firebasejs/12.5.0/firebase-app.js";
   ...
   const app = initializeApp(firebaseConfig);
 </script>
-</script>
 </body>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script>
/* kept behavior intact and fixed start/stop/search without changing UI layout */
let currentUser = null;
const userColors = {};
const config = { iceServers:[{ urls:'stun:stun.l.google.com:19302' }] };
let ws = new WebSocket('ws://localhost:8080');
let peerConnection = null;
let broadcasterPC = null;
let localStream = null;
/* small util */
function randomColor(){
const colors=["#1e90ff","#ff4c4c","#4cff4c","#ffd93d","#a259ff","#ff7f50","#00ced1"];
return colors[Math.floor(Math.random()*colors.length)];
}
/* NAV */
function showSection(id){
document.querySelectorAll('main section').forEach(s=>s.classList.add('hidden'));
document.getElementById(id).classList.remove('hidden');
}
/* LOGIN */
function showLogin(){ document.getElementById('login-modal').classList.remove('hidden'); }
function hideLogin(){ document.getElementById('login-modal').classList.add('hidden'); }
function login(){
const u = document.getElementById('login-user').value.trim();
const p = document.getElementById('login-pass').value.trim();
if(!u || !p) return alert('fill both fields');
currentUser = u;
if(!userColors[u]) userColors[u] = randomColor();
hideLogin();
document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`;
alert('Logged in as '+u);
}
function signup(){ login(); }
function logout(){
currentUser = null;
document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`;
alert('Logged out');
}
/* Go Live button logic */
document.getElementById('go-live-btn').addEventListener('click', ()=>{
if(!currentUser) return alert('Login first');
showSection('dashboard');
});
/* CHAT rendering */
const chatContainer = document.getElementById('chat-container');
const messagesDiv = document.getElementById('messages');
function renderChatLine(user, text){
const p = document.createElement('p');
p.className = 'chat-msg';
const strong = document.createElement('strong');
strong.textContent = user;
strong.style.color = userColors[user] || '#fff';
const span = document.createElement('span');
span.textContent = ': ' + text;
p.appendChild(strong);
p.appendChild(span);
// append to both viewer chat and dashboard messages if present
if(chatContainer) chatContainer.appendChild(p.cloneNode(true));
if(messagesDiv) messagesDiv.appendChild(p.cloneNode(true));
if(chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
if(messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
/* sending chat */
document.getElementById('chat-send-btn').addEventListener('click', sendMessage);
function sendMessage(){
const input = document.getElementById('chat-input');
const msg = input.value.trim();
if(!currentUser) return alert('Login first');
if(!msg) return;
ws.send(JSON.stringify({ type:'chat', chat:msg, user:currentUser }));
renderChatLine(currentUser, msg);
input.value = '';
}
document.getElementById('chat-input').addEventListener('keypress', e=>{ if(e.key==='Enter') sendMessage(); });
function sendMessageDashboard(){
const input = document.getElementById('chatInputDashboard');
const msg = input.value.trim();
if(!currentUser) return alert('Login first');
if(!msg) return;
ws.send(JSON.stringify({ type:'chat', chat:msg, user:currentUser }));
renderChatLine(currentUser, msg);
input.value = '';
}
document.getElementById('chatInputDashboard').addEventListener('keypress', e=>{ if(e.key==='Enter') sendMessageDashboard(); });
/* START / STOP streaming logic fixed and intact */
const startBtn = document.getElementById('start-stream-btn');
const stopBtn = document.getElementById('stop-stream-btn');
const broadcasterVideo = document.getElementById('broadcasterVideo');
startBtn.addEventListener('click', async ()=>{
if(!currentUser) return alert('Login first');
startBtn.classList.add('hidden');
stopBtn.classList.remove('hidden');
// get camera
try{
localStream = await navigator.mediaDevices.getUserMedia({ video:true, audio:true });
broadcasterVideo.srcObject = localStream;
// create PeerConnection for broadcaster (it will send offer to others via ws)
broadcasterPC = new RTCPeerConnection(config);
localStream.getTracks().forEach(t=>broadcasterPC.addTrack(t, localStream));
broadcasterPC.onicecandidate = ({candidate})=>{
if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate, forStreamOwner: true }));
};
const offer = await broadcasterPC.createOffer();
await broadcasterPC.setLocalDescription(offer);
// tell server about offer and new stream
const title = document.getElementById('stream-title').value || 'Untitled';
const category = document.getElementById('stream-category').value || 'chat';
ws.send(JSON.stringify({ type:'offer', offer }));
ws.send(JSON.stringify({ type:'newStream', title, category, user: currentUser }));
// optional show stream key placeholder unchanged
}catch(err){
console.error('start stream failed', err);
alert('could not start stream check camera permissions');
startBtn.classList.remove('hidden');
stopBtn.classList.add('hidden');
}
});
stopBtn.addEventListener('click', ()=>{
stopBtn.classList.add('hidden');
startBtn.classList.remove('hidden');
// notify server
ws.send(JSON.stringify({ type:'stopStream' }));
// close pc and tracks
try{
if(broadcasterPC){
broadcasterPC.getSenders().forEach(s=>broadcasterPC.removeTrack(s));
broadcasterPC.close();
broadcasterPC = null;
}
if(localStream){
localStream.getTracks().forEach(t=>t.stop());
broadcasterVideo.srcObject = null;
localStream = null;
}
}catch(e){ console.warn(e); }
});
/* COPY KEY */
function copyKey(){ navigator.clipboard.writeText(document.getElementById('stream-key').innerText); alert('Copied!'); }
/* SEARCH */
function searchStreams(){
const q = document.getElementById('search-input').value.trim();
ws.send(JSON.stringify({ type:'search', query: q }));
}
function renderResults(list){
const container = document.getElementById('search-results');
container.innerHTML = '';
if(!list || list.length === 0){
// keep same UI feel just show empty
return;
}
list.forEach(s=>{
const card = document.createElement('div');
card.className = 'card';
// show only title as requested and keep UI unchanged
card.textContent = s.title + (s.live ? ' (LIVE)' : '');
card.addEventListener('click', ()=> {
// show viewer section and send request to watch that stream
document.getElementById('viewer-title').textContent = s.title;
document.getElementById('stream-id').textContent = s.id || '';
showSection('viewer');
// prepare to receive offer from owner if server will forward it
// create peerConnection to receive remote track
if(peerConnection){
try{ peerConnection.close(); } catch(e){}
peerConnection = null;
}
peerConnection = new RTCPeerConnection(config);
peerConnection.ontrack = (e) => {
document.getElementById('stream-player').srcObject = e.streams[0];
};
peerConnection.onicecandidate = ({candidate})=>{
if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate, watchingStreamId: s.id }));
};
// tell server we want to watch this stream will trigger server to forward offer or instruct owner
ws.send(JSON.stringify({ type:'watchStream', id: s.id }));
});
container.appendChild(card);
});
}
/* WEBSOCKET message handling */
ws.addEventListener('open', ()=>console.log('ws open'));
ws.addEventListener('error', (e)=>console.error('ws err', e));
ws.addEventListener('message', async ev=>{
try{
const msg = JSON.parse(ev.data);
// chat
if(msg.type === 'chat'){
// server broadcasts chat as {type:'chat', user, chat}
renderChatLine(msg.user, msg.chat);
}
// streams update broadcast maybe used to autofill featured or results
else if(msg.type === 'streamsUpdate'){
// server sends full streams array
// we keep UI intact so only render to search results area when a query was made by server
// but to keep user convenience update featured streams quickly minimal update
// we do not alter layout just update featured list small
const featured = document.getElementById('featured-streams');
featured.innerHTML = '';
msg.streams.slice(0,6).forEach(s=>{
const c = document.createElement('div');
c.className = 'card';
c.textContent = s.title;
featured.appendChild(c);
});
}
// search results
else if(msg.type === 'searchResults'){
renderResults(msg.results || []);
}
// WebRTC offer arrives from stream owner forwarded by server
else if(msg.type === 'offer' || msg.offer){
const offerMsg = msg.offer || msg;
// viewer side answer flow
if(peerConnection === null){
peerConnection = new RTCPeerConnection(config);
peerConnection.ontrack = (e)=>document.getElementById('stream-player').srcObject = e.streams[0];
peerConnection.onicecandidate = ({candidate})=>{
if(candidate) ws.send(JSON.stringify({ type:'iceCandidate', iceCandidate:candidate }));
};
}
await peerConnection.setRemoteDescription(new RTCSessionDescription(offerMsg.offer || offerMsg));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
ws.send(JSON.stringify({ type:'answer', answer }));
}
// answer for broadcaster
else if(msg.type === 'answer' || msg.answer){
const answerMsg = msg.answer || msg;
if(broadcasterPC){
await broadcasterPC.setRemoteDescription(new RTCSessionDescription(answerMsg.answer || answerMsg));
} else if(peerConnection){
await peerConnection.setRemoteDescription(new RTCSessionDescription(answerMsg.answer || answerMsg));
}
}
// ice candidates
else if(msg.type === 'iceCandidate' || msg.iceCandidate){
const cand = msg.iceCandidate || msg;
try{
if(peerConnection) await peerConnection.addIceCandidate(new RTCIceCandidate(cand));
if(broadcasterPC) await broadcasterPC.addIceCandidate(new RTCIceCandidate(cand));
}catch(e){ console.warn('ice add err', e); }
}
}catch(e){
console.error('ws msg parse err', e);
}
});
/* render chat lines when user sends or incoming handled above */
function renderChatLine(user, text){
const p = document.createElement('p');
p.textContent = user + ': ' + text;
// append to both viewer and dashboard
const viewerChat = document.getElementById('chat-container');
const dashChat = document.getElementById('messages');
if(viewerChat) viewerChat.appendChild(p.cloneNode(true));
if(dashChat) dashChat.appendChild(p.cloneNode(true));
}
/* keep old dummy featured example so UI not empty */
(function seedDummy(){
const f = document.getElementById('featured-streams');
if(f) {
if(f.children.length === 0){
const a = document.createElement('div'); a.className='card'; a.textContent='Stream 1'; f.appendChild(a);
const b = document.createElement('div'); b.className='card'; b.textContent='Stream 2'; f.appendChild(b);
const c = document.createElement('div'); c.className='card'; c.textContent='Stream 3'; f.appendChild(c);
}
}
})();
<script type="module">
// Import the functions you need from the SDKs you need
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.5.0/firebase-app.js";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyAyiiWVeRjxSsxUE8fR1DyCPEZwt7o3iHc",
authDomain: "plexstream-ef9b4.firebaseapp.com",
projectId: "plexstream-ef9b4",
storageBucket: "plexstream-ef9b4.firebasestorage.app",
messagingSenderId: "130967153299",
appId: "1:130967153299:web:e424baeb43541f1401bb66"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
</script>
</script>
})();
</script>
<script type="module">
// Import the functions you need from the SDKs you need
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.5.0/firebase-app.js";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyAyiiWVeRjxSsxUE8fR1DyCPEZwt7o3iHc",
authDomain: "plexstream-ef9b4.firebaseapp.com",
projectId: "plexstream-ef9b4",
storageBucket: "plexstream-ef9b4.firebasestorage.app",
messagingSenderId: "130967153299",
appId: "1:130967153299:web:e424baeb43541f1401bb66"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
</script>
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 172-172: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: document.getElementById('user-login').innerHTML = <span>${u}</span> <button onclick="logout()">Logout</button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)


[warning] 178-178: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: document.getElementById('user-login').innerHTML = <button onclick="showLogin()">Login / Signup</button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)


[warning] 144-144: Avoid insecure (ws://) WebSocket connections; use the encrypted wss:// scheme.
Context: new WebSocket('ws://localhost:8080')
Note: [CWE-319] Cleartext Transmission of Sensitive Information.

(insecure-websocket)

🪛 Betterleaks (1.7.0)

[high] 449-449: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.

(gcp-api-key)

🪛 HTMLHint (1.9.2)

[error] 459-459: Tag must be paired, no start tag: [ </script> ]

(tag-pair)

Source: Linters/SAST tools

Comment thread old-stuff/index.html
Comment on lines +166 to +181
function login(){
const u = document.getElementById('login-user').value.trim();
const p = document.getElementById('login-pass').value.trim();
if(!u || !p) return alert('fill both fields');
currentUser = u;
if(!userColors[u]) userColors[u] = randomColor();
hideLogin();
document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`;
alert('Logged in as '+u);
}
function signup(){ login(); }
function logout(){
currentUser = null;
document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`;
alert('Logged out');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Unescaped username injected via innerHTML (DOM-based self-XSS).

u is raw user input from #login-user (line 167) and gets concatenated straight into innerHTML (line 173) with no escaping. A malicious string typed into the username field (e.g. an onerror-bearing tag) would execute in the user's own page. The file already has the safe pattern nearby — renderChatLine builds elements via createElement/textContent (lines 193-208) instead of innerHTML.

🛡️ Suggested fix
     hideLogin();
-    document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`;
+    const loginDiv = document.getElementById('user-login');
+    loginDiv.innerHTML = '';
+    const span = document.createElement('span');
+    span.textContent = u;
+    const btn = document.createElement('button');
+    btn.textContent = 'Logout';
+    btn.addEventListener('click', logout);
+    loginDiv.append(span, ' ', btn);
     alert('Logged in as '+u);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function login(){
const u = document.getElementById('login-user').value.trim();
const p = document.getElementById('login-pass').value.trim();
if(!u || !p) return alert('fill both fields');
currentUser = u;
if(!userColors[u]) userColors[u] = randomColor();
hideLogin();
document.getElementById('user-login').innerHTML = `<span>${u}</span> <button onclick="logout()">Logout</button>`;
alert('Logged in as '+u);
}
function signup(){ login(); }
function logout(){
currentUser = null;
document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`;
alert('Logged out');
}
function login(){
const u = document.getElementById('login-user').value.trim();
const p = document.getElementById('login-pass').value.trim();
if(!u || !p) return alert('fill both fields');
currentUser = u;
if(!userColors[u]) userColors[u] = randomColor();
hideLogin();
const loginDiv = document.getElementById('user-login');
loginDiv.innerHTML = '';
const span = document.createElement('span');
span.textContent = u;
const btn = document.createElement('button');
btn.textContent = 'Logout';
btn.addEventListener('click', logout);
loginDiv.append(span, ' ', btn);
alert('Logged in as '+u);
}
function signup(){ login(); }
function logout(){
currentUser = null;
document.getElementById('user-login').innerHTML = `<button onclick="showLogin()">Login / Signup</button>`;
alert('Logged out');
}
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 172-172: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: document.getElementById('user-login').innerHTML = <span>${u}</span> <button onclick="logout()">Logout</button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)


[warning] 178-178: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: document.getElementById('user-login').innerHTML = <button onclick="showLogin()">Login / Signup</button>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

Source: Linters/SAST tools

Comment thread old-stuff/index.html
Comment on lines +419 to +428
/* render chat lines when user sends or incoming handled above */
function renderChatLine(user, text){
const p = document.createElement('p');
p.textContent = user + ': ' + text;
// append to both viewer and dashboard
const viewerChat = document.getElementById('chat-container');
const dashChat = document.getElementById('messages');
if(viewerChat) viewerChat.appendChild(p.cloneNode(true));
if(dashChat) dashChat.appendChild(p.cloneNode(true));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Duplicate renderChatLine definition silently overrides the colored version.

renderChatLine is already defined at lines 193-208 with per-user color styling via userColors/randomColor(). This second, plain-text definition (function declarations in the same scope, later wins) permanently replaces it, so every chat line renders without color — userColors/randomColor() become dead code.

🔧 Suggested fix
-/* render chat lines when user sends or incoming handled above */
-function renderChatLine(user, text){
-    const p = document.createElement('p');
-    p.textContent = user + ': ' + text;
-    // append to both viewer and dashboard
-    const viewerChat = document.getElementById('chat-container');
-    const dashChat = document.getElementById('messages');
-    if(viewerChat) viewerChat.appendChild(p.cloneNode(true));
-    if(dashChat) dashChat.appendChild(p.cloneNode(true));
-}

Remove this duplicate; the first definition (lines 193-208) already handles both containers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* render chat lines when user sends or incoming handled above */
function renderChatLine(user, text){
const p = document.createElement('p');
p.textContent = user + ': ' + text;
// append to both viewer and dashboard
const viewerChat = document.getElementById('chat-container');
const dashChat = document.getElementById('messages');
if(viewerChat) viewerChat.appendChild(p.cloneNode(true));
if(dashChat) dashChat.appendChild(p.cloneNode(true));
}

Comment thread src/App.jsx
Comment on lines +12 to +17
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add rel="noopener noreferrer" to both new-tab links.

Without it, the opened page may retain window.opener access in affected browsers.

Proposed fix
-        <a href="https://vite.dev" target="_blank">
+        <a href="https://vite.dev" target="_blank" rel="noopener noreferrer">
...
-        <a href="https://react.dev" target="_blank">
+        <a href="https://react.dev" target="_blank" rel="noopener noreferrer">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
<a href="https://vite.dev" target="_blank" rel="noopener noreferrer">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank" rel="noopener noreferrer">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 14-14: target="_blank" without rel="noopener noreferrer" exposes the page to reverse tabnabbing
Context:
Note: [CWE-1022] Use of Web Link to Untrusted Target with window.opener Access.

(jsx-no-target-blank)

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 7

🧹 Nitpick comments (3)
eslint.config.js (1)

16-23: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Remove the conflicting ECMAScript version declarations.

languageOptions.ecmaVersion is 2020, while nested parserOptions.ecmaVersion is latest. Keep one intentional target so parsing and lint rules use the same syntax baseline.

src/index.css (1)

6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve the reported Stylelint violations.

Stylelint flags the empty lines before declarations at Lines 6 and 10 and the casing of optimizeLegibility at Line 11. Fix these or verify that the external Stylelint check is not intended to gate this project.

Source: Linters/SAST tools

old-stuff/styles/style.css (1)

5-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop quotes around "Roboto" per stylelint, and consider deduplicating h2/h3.

Stylelint's font-family-name-quotes flags the quoted font name on lines 6 and 16; h2/h3 are otherwise identical and could share a selector.

🎨 Suggested fix
-h2 {
-  font-family: "Roboto", sans-serif;
+h2, h3 {
+  font-family: Roboto, sans-serif;
   font-optical-sizing: auto;
   font-weight: 300;
   font-style: normal;
   font-variation-settings:
     "wdth" 100;
   color: white;
 }
-
-h3 {
-  font-family: "Roboto", sans-serif;
-  font-optical-sizing: auto;
-  font-weight: 300;
-  font-style: normal;
-  font-variation-settings:
-    "wdth" 100;
-  color: white;
-}

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 76242e88-874b-49cb-ab89-deeac115490f

📥 Commits

Reviewing files that changed from the base of the PR and between 1f57b77 and 63f8b20.

⛔ Files ignored due to path filters (4)
  • old-stuff/favicon.ico is excluded by !**/*.ico
  • package-lock.json is excluded by !**/package-lock.json
  • public/vite.svg is excluded by !**/*.svg
  • src/assets/react.svg is excluded by !**/*.svg
📒 Files selected for processing (16)
  • .gitignore
  • README.md
  • eslint.config.js
  • index.html
  • old-stuff/SECURITY.md
  • old-stuff/index.html
  • old-stuff/server.js
  • old-stuff/signup.html
  • old-stuff/styles/style.css
  • old-stuff/templates/multistream.html
  • package.json
  • src/App.css
  • src/App.jsx
  • src/index.css
  • src/main.jsx
  • vite.config.js
🛑 Comments failed to post (2)
old-stuff/server.js (1)

41-54: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Missing watchStream handler breaks viewing entirely; signaling is broadcast to everyone instead of routed.

The client sends {type:'watchStream', id} when a viewer clicks a search result (old-stuff/index.html:337), but this dispatcher has no matching branch — the message is silently dropped. The broadcaster's offer is only emitted once, at stream start, to whichever clients happen to be connected then (line 52-54 broadcasts to all except sender). Any viewer who connects or clicks "watch" afterward never triggers a re-send of the offer and can never establish a peer connection. Additionally, routing offer/answer/iceCandidate via a global broadcast rather than to the specific target peer means multiple simultaneous broadcasters/viewers will cross-wire each other's signaling messages.

💡 Suggested direction
+            else if (data.type === "watchStream") {
+                const stream = streams.find((s) => s.id === data.id);
+                ws.watchingId = data.id;
+                if (stream && stream.ownerWs) {
+                    stream.ownerWs.send(JSON.stringify({ type: "requestOffer", forViewer: ws.id }));
+                }
+            }

Track the broadcaster's connection on the stream object and route offer/answer/iceCandidate to the specific target client (by id) instead of broadcasting to everyone.

old-stuff/templates/multistream.html (1)

1-5: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Invalid <centre> tag and unclosed <iframe>.

<centre> isn't a real HTML element (typo for <center>, which is deprecated anyway), so the heading won't actually be centered — the intended layout silently fails. HTMLHint also flags the <iframe> as missing its closing tag.

🔧 Suggested fix
 <!DOCTYPE html>
-    <centre><h1>User Multi-stream</h1></centre>
+    <h1 style="text-align:center">User Multi-stream</h1>
     <iframe id="ytplayer" type="text/html" width="720" height="405"
 src="https://www.youtube.com/embed/ZvZWWJTcXhU?autoplay=1&controls=0&disablekb=1&end=457&fs=0&loop=1&start=448&color=white"
-frameborder="0" allowfullscreen>
+frameborder="0" allowfullscreen></iframe>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

<!DOCTYPE html>
    <h1 style="text-align:center">User Multi-stream</h1>
    <iframe id="ytplayer" type="text/html" width="720" height="405"
src="https://www.youtube.com/embed/ZvZWWJTcXhU?autoplay=1&controls=0&disablekb=1&end=457&fs=0&loop=1&start=448&color=white"
frameborder="0" allowfullscreen></iframe>
🧰 Tools
🪛 HTMLHint (1.9.2)

[error] 5-5: Tag must be paired, missing: [ </iframe> ], open tag match failed [ <iframe id="ytplayer" type="text/html" width="720" height="405" src="https://www.youtube.com/embed/ZvZWWJTcXhU?autoplay=1&controls=0&disablekb=1&end=457&fs=0&loop=1&start=448&color=white" frameborder="0" allowfullscreen> ] on line 3.

(tag-pair)

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant