Skip to content

Instantly share code, notes, and snippets.

@HoughIO
Last active March 11, 2026 04:05
Show Gist options
  • Select an option

  • Save HoughIO/fcb3f7c90fb84c1d6cfa4cd9acee0746 to your computer and use it in GitHub Desktop.

Select an option

Save HoughIO/fcb3f7c90fb84c1d6cfa4cd9acee0746 to your computer and use it in GitHub Desktop.
Kevin & Todd random names update
#!/bin/bash
# Kevin & Todd Cam - Random Names Update
# Run on Pi: bash update-names.sh
echo "🐱 Adding random name generator..."
# Update the index.html with random names
sudo tee /opt/kittycam/web/index.html > /dev/null << 'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#0a0a0a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Asshole Cam</title>
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
:root{--bg:#0a0a0a;--surface:rgba(255,255,255,0.03);--border:rgba(255,255,255,0.08);--text:#fff;--text-dim:rgba(255,255,255,0.5);--accent:#ff6b6b}
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif;min-height:100vh;min-height:100dvh}
.tabs{position:fixed;bottom:0;left:0;right:0;display:flex;background:var(--bg);border-top:1px solid var(--border);padding-bottom:env(safe-area-inset-bottom);z-index:100}
.tab{flex:1;padding:12px;text-align:center;color:var(--text-dim);font-size:.75rem;cursor:pointer;border:none;background:none}
.tab.active{color:var(--accent)}
.tab-icon{font-size:1.4rem;display:block;margin-bottom:4px}
.page{display:none;min-height:100vh;padding-bottom:80px}
.page.active{display:block}
.stream-wrapper{height:calc(100vh - 80px);display:flex;align-items:center;justify-content:center;padding:env(safe-area-inset-top) 0 0 0;background:#000}
#stream{width:100%;height:100%;object-fit:contain;opacity:0;transition:opacity .3s}
#stream.loaded{opacity:1}
.header{position:fixed;top:0;left:0;right:0;padding:calc(env(safe-area-inset-top) + 12px) 16px 12px;display:flex;justify-content:space-between;align-items:center;background:linear-gradient(to bottom,rgba(0,0,0,.8) 0%,transparent 100%);z-index:10}
.title{font-size:.85rem;font-weight:500;display:flex;align-items:center;gap:8px;max-width:60%;overflow:hidden}
.title-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.live-dot{width:8px;height:8px;background:var(--accent);border-radius:50%;animation:pulse 2s infinite;flex-shrink:0}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.quality-pill{display:flex;background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:3px;backdrop-filter:blur(10px);flex-shrink:0}
.quality-btn{padding:5px 12px;border:none;background:transparent;color:var(--text-dim);font-size:.7rem;font-weight:500;text-transform:uppercase;border-radius:14px;cursor:pointer}
.quality-btn.active{background:var(--text);color:var(--bg)}
.page-header{padding:calc(env(safe-area-inset-top) + 16px) 16px 16px;font-size:1.5rem;font-weight:600}
.clips-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:0 8px}
.clip-card{aspect-ratio:16/9;background:var(--surface);border-radius:12px;overflow:hidden;position:relative;cursor:pointer}
.clip-card video{width:100%;height:100%;object-fit:cover}
.clip-time{position:absolute;bottom:8px;left:8px;background:rgba(0,0,0,.7);padding:4px 8px;border-radius:6px;font-size:.7rem}
.events-list{padding:0 16px}
.event-item{display:flex;align-items:center;gap:12px;padding:16px 0;border-bottom:1px solid var(--border)}
.event-icon{width:40px;height:40px;background:var(--surface);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.2rem}
.event-info{flex:1}
.event-message{font-size:.9rem}
.event-time{font-size:.75rem;color:var(--text-dim);margin-top:2px}
.event-clip-btn{background:var(--accent);color:#fff;border:none;padding:6px 12px;border-radius:12px;font-size:.75rem;cursor:pointer}
.empty-state{text-align:center;padding:60px 20px;color:var(--text-dim)}
.empty-state-icon{font-size:3rem;margin-bottom:16px}
.modal{position:fixed;inset:0;background:rgba(0,0,0,.95);z-index:1000;display:none;align-items:center;justify-content:center}
.modal.active{display:flex}
.modal video{max-width:100%;max-height:100%}
.modal-close{position:absolute;top:calc(env(safe-area-inset-top) + 16px);right:16px;background:var(--surface);border:none;color:#fff;width:40px;height:40px;border-radius:50%;font-size:1.5rem;cursor:pointer}
.auth-screen{position:fixed;inset:0;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;z-index:2000}
.auth-screen .icon{font-size:4rem}
.auth-screen p{color:var(--text-dim)}
.hidden{display:none!important}
.loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000}
.spinner{width:32px;height:32px;border:2px solid var(--border);border-top-color:var(--text);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.retry-btn{margin-top:16px;padding:12px 24px;background:var(--accent);color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer}
.rarity-badge{font-size:.6rem;padding:2px 6px;border-radius:8px;margin-left:6px;font-weight:bold;text-transform:uppercase}
.rarity-rare{background:#8847ff;color:#fff}
.rarity-ultra{background:#e4ae39;color:#000;animation:goldPulse 2s ease-in-out infinite}
@keyframes goldPulse{0%,100%{box-shadow:0 0 5px #e4ae39,0 0 10px #e4ae39}50%{box-shadow:0 0 15px #e4ae39,0 0 25px #e4ae39}}
.title-normal{color:#fff}
.title-rare{color:#8847ff;text-shadow:0 0 10px rgba(136,71,255,0.5)}
.title-ultra{background:linear-gradient(90deg,#e4ae39,#f7d774,#e4ae39);background-size:200% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;animation:goldShimmer 2s linear infinite}
@keyframes goldShimmer{to{background-position:200% center}}
</style>
</head>
<body>
<div id="auth-screen" class="auth-screen">
<div class="icon">😼</div>
<p>Link required</p>
</div>
<div id="app" class="hidden">
<div id="page-live" class="page active">
<div class="header">
<div class="title">
<div class="live-dot"></div>
<span class="title-text" id="cat-names">Loading...</span>
<span class="rarity-badge hidden" id="rarity-badge"></span>
</div>
<div class="quality-pill">
<button class="quality-btn active" id="btn-hd">HD</button>
<button class="quality-btn" id="btn-save">Lite</button>
</div>
</div>
<div class="stream-wrapper">
<div class="loading" id="loading"><div class="spinner"></div></div>
<div id="error-state" class="hidden" style="text-align:center">
<div style="font-size:3rem;margin-bottom:16px">😿</div>
<p style="color:var(--text-dim)">Stream unavailable</p>
<button class="retry-btn" onclick="startStream()">Retry</button>
</div>
<img id="stream" alt="">
</div>
</div>
<div id="page-clips" class="page">
<div class="page-header">Clips</div>
<div class="clips-grid" id="clips-grid"><div class="empty-state"><div class="empty-state-icon">🎬</div><p>No clips yet</p></div></div>
</div>
<div id="page-activity" class="page">
<div class="page-header">Activity</div>
<div class="events-list" id="events-list"><div class="empty-state"><div class="empty-state-icon">😴</div><p>No motion detected yet</p></div></div>
</div>
<nav class="tabs">
<button class="tab active" data-page="live"><span class="tab-icon">📺</span>Live</button>
<button class="tab" data-page="clips"><span class="tab-icon">🎬</span>Clips</button>
<button class="tab" data-page="activity"><span class="tab-icon">🔔</span>Activity</button>
</nav>
<div class="modal" id="video-modal">
<button class="modal-close" id="modal-close">×</button>
<video id="modal-video" controls playsinline></video>
</div>
</div>
<script>
// Random name generator with rarities
const KEVIN={normal:["Kevin","Kev","Kevers","Kevster","Keyvers","Saint Kevin","Brother Kevin","The Orange Kev","Big Kev","Little Kev","Captain Kevin","Professor Kev","Doctor Kevin","Kevinator","Kevvy","Kevvy Boy","Kevmeister","Kevlord","Kevski","Kevzilla","Kevinsaurus","Kevin McFluff","Snack Bandit Kev","Sleepy Kev","Biscuit Maker Kev","Chaos Kev","Gremlin Kev","The Kevening","Criminal Kev","Outlaw Kev","Kev the Menace","Kev the Destroyer","Kev the Great","Kev the Problem","Kev the Absolute Menace","Kevjamin","Kevothy","Kevbert","Kevnard","Kevathan","Kevtopher","Kevcules","Kevdini","Kevtron","Kevlopotamus","Kevquatch","Kevpocalypse","Kevageddon","Supreme Overkev","Kevdalf the Orange","Kevuman the White","Kevrond Peredhel","Kevagorn Son of Arathorn","Kevomir of Gondor","Kevgolas Greenleaf","Kevimli Son of Gloin","Kevwise the Brave","Kevrodo of the Shire","Kevrond Half-Elven","Kevion of Rivendell","Kevbrandir","Kevendur","Don Kevin Soprano","Kev \\"The Claw\\" Moltisanti","Kev \\"Two-Toes\\" Cifaretto","Kev \\"The Collector\\" Aprile","Kev \\"The Pawshaker\\" Spatafore","Kev \\"The Quiet\\" Gualtieri","Kev \\"Nine Lives\\" Bacala","Kev \\"The Fixer\\" Dante","Kev \\"The Ledger\\" Parisi","Kev \\"The Informer\\" Bonpensiero","Kev \\"The Charmer\\" La Cerva","Kev \\"The Treasurer\\" Barese","Kev \\"The Neighbor\\" Cusamano","Kev \\"The Button Man\\" Palmice","Kev \\"The Collector\\" Giunta","Kev \\"The Boss\\" Lupertazzi","Kev \\"The Animal\\" Blundetto","Kev \\"The Runaway\\" Petrulio","Kev \\"The Wire\\" Altieri","Kev \\"The Associate\\" DeAngelis","Kev \\"The Contractor\\" Scatino","Kev \\"The Muscle\\" Pontecorvo","Kev \\"The Bookkeeper\\" Melfi","Kev \\"The Gambler\\" Tannenbaum","Kev \\"The Driver\\" Walnuts","Kev \\"The Couch Goblin\\"","Kev \\"The Treat Extortionist\\"","Kev \\"The Orange Menace\\""],rare:["Asshole 1","Asshole Kev","Kev the Asshole","Dumbass Kevin","Bastard Kev","Kev the Little Bastard","Little Shit Kev","Kev the Shithead","Kev the Dumbass"],ultraRare:["Kevin the Cunt"]};
const TODD={normal:["Todd","Todders","Toddles","Todd-diddly-dumkins","Todd the Malevolent","Big Todd","Little Todd","The Grey Todd","Sleepy Todd","Snack Bandit Todd","Gremlin Todd","Chaos Todd","Biscuit Maker Todd","Criminal Todd","Outlaw Todd","Todd the Menace","Todd the Destroyer","Todd the Problem","Todd the Absolute Menace","The Toddening","Toddjamin","Toddothy","Toddbert","Toddnard","Toddathan","Toddcules","Toddtron","Toddquatch","Toddopotamus","Toddageddon","Todd McFluff","Todd the Couch Goblin","Todd the Treat Extortionist","Todd the Blanket Tyrant","Todd the Window Inspector","Todd the Carpet General","Todd the Sock Thief","Todd the Laundry Guardian","Todd the Naplord","Todd the Radiator King","Todd the Grey Wanderer","Todd the White Councilor","Todd of the Shire","Todd the Ring Sneak","Todd of Gondor","Todd the Mirkwood Stalker","Todd of Erebor","Todd the Rivendell Guest","Todd the Lothlórien Lurker","Todd the Lonely Mountain Bandit","Todd the Second Breakfast Inspector","Todd the Palantír Watcher","Todd the Grey Havens Drifter","Todd the Shire Troublemaker","Todd the Mordor Scout","Todd the Uruk Botherer","Todd the Fellowship Stowaway","Todd the Entish Observer","Todd the Bree Tavern Lurker","Todd the Rohan Stable Raider","Todd \"Velvet Paws\" Soprano","Todd \"Grey Shadow\" Moltisanti","Todd \"Loose Whiskers\" Cifaretto","Todd \"The Nap Boss\" Aprile","Todd \"Soft Claws\" Spatafore","Todd \"The Carpet King\" Gualtieri","Todd \"Whisker Face\" Bacala","Todd \"The Radiator Don\" Dante","Todd \"Side-Eye\" Parisi","Todd \"The Couch Fixer\" Bonpensiero","Todd \"Smooth Paws\" La Cerva","Todd \"Quiet Feet\" Barese","Todd \"Backyard Boss\" Cusamano","Todd \"Whisker Knife\" Palmice","Todd \"The Treat Collector\" Giunta","Todd \"The Grey Don\" Lupertazzi","Todd \"Nine Nap\" Blundetto","Todd \"Window Watcher\" Petrulio","Todd \"Silent Paw\" Altieri","Todd \"Porch Boss\" DeAngelis","Todd \"The Ledger Paw\" Scatino","Todd \"Heavy Step\" Pontecorvo","Todd \"The Doctor's Friend\" Melfi","Todd \"Card Shark\" Tannenbaum","Todd \"Walnut Paws\" Gualtieri","Todd the Grey Menace","Todd the Couch Inspector","Todd the Treat Bandit","Todd the Radiator Goblin","Todd the Blanket Invader"],rare:["Tardles","Asshole Todd","Todd the Asshole","Dumbass Todd","Bastard Todd","Todd the Little Bastard","Little Shit Todd","Todd the Shithead","Todd the Dumbass"],ultraRare:["Todd the Super-Cunt"]};
function pickName(obj){const w=[],n=[];obj.normal.forEach(x=>{n.push({name:x,rarity:'normal'});w.push(1)});obj.rare.forEach(x=>{n.push({name:x,rarity:'rare'});w.push(0.627)});obj.ultraRare.forEach(x=>{n.push({name:x,rarity:'ultra'});w.push(0.47)});const t=w.reduce((a,b)=>a+b,0);let r=Math.random()*t;for(let i=0;i<n.length;i++){r-=w[i];if(r<=0)return n[i]}return n[0]}
function setRandomNames(){const k=pickName(KEVIN),t=pickName(TODD);const titleEl=document.getElementById('cat-names');titleEl.textContent=k.name+' & '+t.name;const badge=document.getElementById('rarity-badge');titleEl.className='title-text';if(k.rarity==='ultra'||t.rarity==='ultra'){badge.textContent='★ CONTRABAND';badge.className='rarity-badge rarity-ultra';badge.classList.remove('hidden');titleEl.classList.add('title-ultra')}else if(k.rarity==='rare'||t.rarity==='rare'){badge.textContent='RESTRICTED';badge.className='rarity-badge rarity-rare';badge.classList.remove('hidden');titleEl.classList.add('title-rare')}else{badge.classList.add('hidden');titleEl.classList.add('title-normal')}}
const STORAGE_KEY='kittycam_auth',MODE_KEY='kittycam_mode';
let isHD=true,snapshotInterval=null,streamRetries=0;
function getAuthKey(){const p=new URLSearchParams(location.search),k=p.get('key');if(k){localStorage.setItem(STORAGE_KEY,k);history.replaceState({},'',location.pathname);return k}return localStorage.getItem(STORAGE_KEY)}
async function validateAuth(k){try{return(await fetch('/auth?key='+k)).ok}catch{return!!localStorage.getItem(STORAGE_KEY)}}
function startStream(){const s=document.getElementById('stream'),l=document.getElementById('loading'),e=document.getElementById('error-state');s.classList.remove('loaded');l.classList.remove('hidden');e.classList.add('hidden');if(snapshotInterval){clearInterval(snapshotInterval);snapshotInterval=null}s.onerror=()=>{streamRetries++;if(streamRetries<3){setTimeout(startStream,2000)}else{l.classList.add('hidden');e.classList.remove('hidden')}};s.onload=()=>{streamRetries=0;s.classList.add('loaded');l.classList.add('hidden')};if(isHD){s.src='/stream?action=stream&t='+Date.now()}else{const u=()=>{s.src='/stream?action=snapshot&t='+Date.now()};u();snapshotInterval=setInterval(u,1500)}}
function setMode(m){isHD=m==='hd';localStorage.setItem(MODE_KEY,m);document.getElementById('btn-hd').classList.toggle('active',isHD);document.getElementById('btn-save').classList.toggle('active',!isHD);streamRetries=0;startStream()}
function switchPage(n){document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));document.getElementById('page-'+n).classList.add('active');document.querySelector('[data-page="'+n+'"]').classList.add('active');if(n==='clips')loadClips();if(n==='activity')loadEvents()}
async function loadClips(){try{const r=await fetch('/clips/'),f=await r.json(),g=document.getElementById('clips-grid');if(!f.length){g.innerHTML='<div class="empty-state"><div class="empty-state-icon">🎬</div><p>No clips yet</p></div>';return}const c=f.filter(x=>x.name&&x.name.endsWith('.mp4')).sort((a,b)=>b.name.localeCompare(a.name)).slice(0,20);g.innerHTML=c.map(x=>{const m=x.name.match(/(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})/);let t=x.name;if(m){const d=new Date(m[1],m[2]-1,m[3],m[4],m[5]);t=d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}return'<div class="clip-card" data-src="/clips/'+x.name+'"><video src="/clips/'+x.name+'" muted preload="metadata"></video><div class="clip-time">'+t+'</div></div>'}).join('');g.querySelectorAll('.clip-card').forEach(c=>c.onclick=()=>playClip(c.dataset.src))}catch(e){console.error(e)}}
function playClip(s){const m=document.getElementById('video-modal'),v=document.getElementById('modal-video');v.src=s;m.classList.add('active');v.play()}
async function loadEvents(){try{const r=await fetch('/api/events'),e=await r.json(),l=document.getElementById('events-list');if(!e.length){l.innerHTML='<div class="empty-state"><div class="empty-state-icon">😴</div><p>No motion detected yet</p></div>';return}l.innerHTML=e.reverse().slice(0,50).map(x=>'<div class="event-item"><div class="event-icon">'+(x.type==='motion_start'?'🐱':'🎬')+'</div><div class="event-info"><div class="event-message">'+x.message+'</div><div class="event-time">'+x.datetime+'</div></div>'+(x.clip?'<button class="event-clip-btn" onclick="playClip(\'/clips/'+x.clip+'\')">Play</button>':'')+'</div>').join('')}catch(e){console.error(e)}}
async function init(){const k=getAuthKey();if(!k||!(await validateAuth(k))){document.getElementById('auth-screen').classList.remove('hidden');return}document.getElementById('auth-screen').classList.add('hidden');document.getElementById('app').classList.remove('hidden');setRandomNames();if(localStorage.getItem(MODE_KEY)==='save'){isHD=false;document.getElementById('btn-hd').classList.remove('active');document.getElementById('btn-save').classList.add('active')}document.getElementById('btn-hd').onclick=()=>setMode('hd');document.getElementById('btn-save').onclick=()=>setMode('save');document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>switchPage(t.dataset.page));document.getElementById('modal-close').onclick=()=>{document.getElementById('video-modal').classList.remove('active');document.getElementById('modal-video').pause()};document.getElementById('cat-names').onclick=()=>setRandomNames();startStream()}
if('serviceWorker'in navigator){navigator.serviceWorker.getRegistrations().then(r=>r.forEach(x=>x.update()));navigator.serviceWorker.register('/sw.js')}
init();
</script>
</body>
</html>
HTMLEOF
echo "✓ Random names installed!"
echo ""
echo "Features:"
echo " • Random Kevin & Todd names on each page load"
echo " • Tap the name to get a new random one"
echo " • Rare names show 💀 badge"
echo " • Ultra rare names show ✨ badge"
echo ""
echo "Refresh the page to see it!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment