Created
March 11, 2026 04:21
-
-
Save HoughIO/b07568101ad8c2b08b852eeffcbbc5cb to your computer and use it in GitHub Desktop.
Kittycam fix - clean names
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Quick fix - restore working names without fancy quotes | |
| 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:70%;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} | |
| .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> | |
| const KEVIN={normal:["Kevin","Kev","Kevers","Kevster","Saint Kevin","The Orange Kev","Big Kev","Little Kev","Captain Kevin","Professor Kev","Doctor Kevin","Kevinator","Kevvy","Kevmeister","Kevlord","Kevzilla","Kevinsaurus","Kevin McFluff","Snack Bandit Kev","Sleepy Kev","Chaos Kev","Gremlin Kev","The Kevening","Criminal Kev","Outlaw Kev","Kev the Menace","Kev the Destroyer","Kev the Great","Kevjamin","Kevothy","Kevbert","Kevcules","Kevdini","Kevtron","Kevpocalypse","Kevageddon","Supreme Overkev","Kevdalf the Orange","Kevuman the White","Kevagorn Son of Arathorn","Kevomir of Gondor","Kevgolas Greenleaf","Kevimli Son of Gloin","Kevwise the Brave","Don Kevin Soprano","Kev Nine Lives Bacala","Kev the Boss Lupertazzi","Kev the Animal Blundetto","Kev the Couch Goblin","Kev the Treat Extortionist","Kev the Orange Menace"],rare:["Asshole Kev","Kev the Asshole","Dumbass Kevin","Bastard Kev","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","Gremlin Todd","Chaos Todd","Criminal Todd","Outlaw Todd","Todd the Menace","Todd the Destroyer","The Toddening","Toddjamin","Toddothy","Toddbert","Toddcules","Toddtron","Toddageddon","Todd McFluff","Todd the Couch Goblin","Todd the Treat Extortionist","Todd the Blanket Tyrant","Todd the Naplord","Todd the Radiator King","Todd the Grey Wanderer","Todd of the Shire","Todd of Gondor","Todd of Erebor","Todd the Second Breakfast Inspector","Todd Velvet Paws Soprano","Todd The Radiator Don Dante","Todd The Grey Don Lupertazzi","Todd Walnut Paws Gualtieri","Todd the Grey Menace","Todd the Treat Bandit"],rare:["Tardles","Asshole Todd","Todd the Asshole","Dumbass Todd","Bastard Todd","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></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()} | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |
| HTMLEOF | |
| echo "β Fixed! Refresh with your key URL" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment