... ... @media (prefers-reduced-motion: reduce) { }
00:00
00:00
โ€”
Swipe up or press Enter
HQ โ€” iPadOS-style shell CSS (HIG-tuned, expanded) - Tokenized color system - Motion curves - Accessibility (reduced motion, focus) ========================================================================== */ :root{ /* HIG palette (dark baseline) */ --bg:#0b0b0d; --bg-elev:#0e1118; --panel:#101115cc; /* translucent panels */ --line:#1e2027; /* separators */ --ink:#e6e7eb; /* primary text */ --muted:#9aa1ac; /* secondary text */ --acc:#2f7cf6; /* accent / tint */ --success:#2dd4bf; --danger:#ef4444; /* Surfaces */ --icon-bg:#171a22; --dock:#12131add; /* Blur/Depth */ --radius:16px; --blur:18px; --shadow:0 24px 80px rgba(0,0,0,.45); /* Motion (HIG-ish) */ --ease: cubic-bezier(.22,.61,.36,1); --fast: 160ms; --norm: 260ms; } @media (prefers-color-scheme: light){ :root{ --bg:#f5f6f8; --bg-elev:#ffffff; --panel:#ffffffcc; --line:#e4e7ef; --ink:#0a0c10; --muted:#5a6270; --icon-bg:#eef1f6; --dock:#f8f9fbcc; --shadow:0 16px 54px rgba(0,0,0,.15); } } /* Base */ *{box-sizing:border-box} html,body{margin:0;height:100%} html{background:var(--bg)} body{ color:var(--ink); font:14px/1.5 -apple-system,BlinkMacSystemFont,Inter,Segoe UI,Roboto,Arial,sans-serif; background:var(--bg); overflow:hidden; -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility; } /* Accessibility */ .sr-only{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden} :focus-visible{ outline:2px solid var(--acc); outline-offset:3px; border-radius:10px; } /* Status bar */ #statusbar{ position:fixed; top:0; left:0; right:0; height:28px; display:flex; align-items:center; justify-content:space-between; padding: env(safe-area-inset-top) 10px 0 10px; background:color-mix(in srgb, var(--bg) 92%, transparent); border-bottom:1px solid var(--line); backdrop-filter: blur(var(--blur)); z-index:50; } .sb-left,.sb-right{display:flex;gap:8px;align-items:center} .sb-center{font-weight:600;font-variant-numeric:tabular-nums} .sb-pill{width:16px;height:10px;border:1px solid var(--muted);border-radius:3px;display:inline-block} .sb-battery{width:28px;height:12px;border:1px solid var(--muted);border-radius:3px;position:relative} .sb-battery .level{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,#7ce38b,#2dd4bf);width:72%} /* Screens */ .screen{position:absolute;inset:28px 0 0 0;display:none} .screen.active{display:block} /* Lock */ #lockscreen{ display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px; background: radial-gradient(120% 150% at 50% -20%, #171b29, #0a0d14 60%, #07090d 100%); } .lock-center{text-align:center;transform:translateY(-6vh)} .lock-clock{font-size:clamp(56px,10vw,96px);font-weight:700;letter-spacing:0.5px} .lock-date{color:var(--muted)} .lock-hint{margin-top:10px;color:var(--muted)} .lock-actions{position:absolute;bottom:26px;left:0;right:0;display:flex;justify-content:space-between;padding:0 20px} .lock-btn{ background:rgba(16,17,21,.66);border:1px solid var(--line);border-radius:999px;width:46px;height:46px;color:var(--ink); backdrop-filter:blur(var(--blur)); transition: transform var(--fast) var(--ease), box-shadow var(--fast) var(--ease); } .lock-btn:active{transform: scale(.96)} /* Home / Springboard */ #homescreen{display:flex;flex-direction:column} .pages{ position:absolute; inset:28px 0 100px 0; overflow:auto; display:grid; grid-auto-flow:column; grid-auto-columns:100%; scroll-snap-type:x mandatory; scroll-behavior:smooth; } .page{ width:100%; height:100%; display:grid; grid-template-columns:repeat(auto-fill,minmax(90px,1fr)); gap:18px; align-content:start; justify-items:center; padding:24px; scroll-snap-align:center; } .icon-wrap{display:flex;flex-direction:column;align-items:center;gap:6px} .icon{ position:relative; width:76px; height:76px; border:none; cursor:pointer; border-radius:22%; background:var(--icon-bg); display:flex; align-items:center; justify-content:center; font-size:34px; color:var(--ink); box-shadow:0 6px 18px rgba(0,0,0,.25); transition: transform var(--fast) var(--ease), box-shadow var(--fast) var(--ease); } .icon:hover{transform:translateY(-2px); box-shadow:0 8px 24px rgba(0,0,0,.35)} .icon:active{transform:scale(.97)} .icon .del{ position:absolute; top:-8px; right:-8px; width:22px; height:22px; border:1px solid var(--line); background:#2a2e36; color:#fff; border-radius:999px; display:none; align-items:center; justify-content:center; font-size:14px } .icon-label{margin-top:6px;color:var(--muted);font-size:12px;text-align:center;max-width:88px} .icon-wrap.jiggle .icon{animation:jiggle .16s linear infinite alternate} @keyframes jiggle{from{transform:rotate(-2deg)}to{transform:rotate(2deg)}} .page-dots{position:absolute;left:50%;transform:translateX(-50%);bottom:88px;display:flex;gap:8px} .page-dots .dot{width:6px;height:6px;border-radius:999px;background:#465;opacity:.4} .page-dots .dot.active{opacity:1;background:#9ab} /* Dock */ .dock{ position:absolute; left:50%; transform:translateX(-50%); bottom:16px; display:flex; gap:12px; background:var(--dock); border:1px solid var(--line); border-radius:18px; padding:12px 14px; backdrop-filter:blur(var(--blur)); box-shadow:0 14px 46px rgba(0,0,0,.38); align-items:center } .dock .icon{width:62px;height:62px;font-size:28px} .dock-divider{width:1px;height:44px;background:var(--line);opacity:.7;margin:0 2px} /* Windows */ #applayer{position:absolute;inset:28px 0 0 0;pointer-events:none} .win{ position:absolute; inset:28px 12px 12px 12px; background:var(--bg-elev); border:1px solid var(--line); border-radius:16px; box-shadow:var(--shadow); transform-origin:50% 100%; opacity:0; transform:scale(.98) translateY(20px); transition:all var(--norm) var(--ease); pointer-events:auto; overflow:hidden } .win.active{opacity:1;transform:scale(1) translateY(0)} .win-header{height:44px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;border-bottom:1px solid var(--line);background:linear-gradient(180deg,#111318,#0c0e13)} .win-title{font-weight:600} .win-actions{display:flex;gap:8px} .win-actions button{ background:#181b22;border:1px solid var(--line);border-radius:8px;color:var(--ink);padding:6px 10px; transition: background var(--fast) var(--ease) } .win-actions button:hover{background:#1c2029} /* Switcher */ #switcher{position:absolute;inset:28px 0 0 0;display:none;align-items:flex-end;gap:16px;padding:24px;background:linear-gradient(180deg,rgba(0,0,0,.1),rgba(0,0,0,.5))} #switcher.active{display:flex} .card{width:240px;height:320px;background:#0f1219;border:1px solid var(--line);border-radius:16px;box-shadow:0 10px 40px rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;color:var(--muted)} /* Control Center */ #cc{position:absolute;right:16px;top:40px;width:360px;background:var(--panel);border:1px solid var(--line);border-radius:16px;box-shadow:0 12px 44px rgba(0,0,0,.5);display:none;padding:12px;backdrop-filter:blur(var(--blur))} #cc.active{display:block} .cc-grabber{width:40px;height:4px;background:#2a2e36;border-radius:999px;margin:0 auto 10px} .cc-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px} .cc-tile{height:68px;border-radius:999px;background:#141722;border:1px solid var(--line);color:var(--ink);font-weight:600} .cc-tile[aria-pressed="true"]{outline:3px solid var(--acc);outline-offset:0} .cc-slider{grid-column:1/-1;background:#141722;border:1px solid var(--line);border-radius:16px;padding:12px} .cc-slider input{width:100%;height:28px} /* NC + Today */ #nc{position:absolute;left:50%;transform:translateX(-50%);top:40px;width:min(860px,92vw);background:var(--panel);border:1px solid var(--line);border-radius:16px;display:none;padding:12px;backdrop-filter:blur(var(--blur))} #nc.active{display:block} .nc-grabber{width:40px;height:4px;background:#2a2e36;border-radius:999px;margin:0 auto 10px} .today{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:10px} .widget{background:#141722;border:1px solid var(--line);border-radius:12px;padding:10px;min-height:110px} .nc-list{display:flex;flex-direction:column;gap:10px} .notif{background:#141722;border:1px solid var(--line);border-radius:12px;padding:10px} .notif .n-title{font-weight:600} .notif .n-time{color:var(--muted);font-size:12px} /* Spotlight */ #spotlight{position:absolute;left:50%;transform:translateX(-50%);top:60px;width:min(720px,92vw);display:none;gap:10px;flex-direction:column;backdrop-filter:blur(22px)} #spotlight.active{display:flex} #spot-input{width:100%;border-radius:12px;border:1px solid var(--line);background:#0f1219;color:var(--ink);padding:12px 14px;font-size:16px} #spot-results{background:#0f1219;border:1px solid var(--line);border-radius:12px;max-height:50vh;overflow:auto} .spot-item{padding:10px 14px;border-bottom:1px solid #1b2029} .spot-item:last-child{border-bottom:none} /* Toasts */ #toasts{position:absolute;left:50%;transform:translateX(-50%);bottom:110px;display:flex;flex-direction:column;gap:8px;pointer-events:none} .toast{background:#11151e;border:1px solid var(--line);border-radius:10px;padding:10px 12px;color:var(--ink);box-shadow:0 10px 30px rgba(0,0,0,.5);transition:opacity var(--fast) var(--ease)} /* Modals */ .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:999} .modal{background:#0e1118;border:1px solid var(--line);border-radius:12px;padding:14px;min-width:300px;max-width:92vw} .modal-title{font-weight:700;margin-bottom:8px} .modal-help{color:var(--muted);font-size:13px;margin-bottom:6px} .modal-input{width:100%;padding:8px;border:1px solid var(--line);border-radius:8px;background:#0f1219;color:#fff;margin:6px 0} .modal-q{margin:6px 0;color:var(--ink)} .modal-row{display:flex;gap:8px;align-items:center;justify-content:flex-end;margin-top:6px} button.accent{background:var(--acc);border:1px solid color-mix(in srgb, var(--acc) 70%, #1e5ccd);color:#fff;border-radius:8px;padding:6px 10px} button.accent:hover{filter:brightness(1.05)} button.link{background:none;border:none;color:#8fb2ff;text-decoration:underline;cursor:pointer} #face-video{display:block;width:360px;max-width:80vw;border-radius:8px;border:1px solid var(--line);background:#000} /* Safe Area for iOS notch etc */ @supports(padding:max(0px)){ #statusbar{ padding-top: max(env(safe-area-inset-top), 0px); } .dock{ margin-bottom: max(env(safe-area-inset-bottom), 12px); } } # ---------------- os.js (frontend logic โ€” FULL HIG) ---------------- info "โœ๏ธ Writing JS (os.js โ€” expanded)" cat > "/os.js" <<'JS' (function(){ "use strict"; /* ========================================================== HQ โ€” iPadOS-style shell (frontend) - HIG interaction model - Lock security (passcode + experimental Face ID) - Relock timeout; intruder snapshots; optional alert webhook - Control Center / Notification Center / Spotlight / Switcher - Apps (Files, Photos+Videos with Set-as-wallpaper, Camera, Notes, Reminders, Calendar, Mail placeholder, Browser, Weather, Terminal, App Store for clock/calculator extras) ========================================================== */ // -------- DOM helpers -------- const $ = (s,sc)=> (sc||document).querySelector(s); const $$ = (s,sc)=> Array.from((sc||document).querySelectorAll(s)); const sleep = (ms)=> new Promise(r=>setTimeout(r,ms)); // -------- Persistent keys -------- const K = { passcode: "hq.auth.passcode", // sha-256 hex faceTpl: "hq.auth.face.tpl", // base64 cropped template faceOn: "hq.auth.face.on", // "1" | "" question: "hq.auth.recov.q", answer: "hq.auth.recov.a.hash", // sha-256 hex relock: "hq.lock.relockSecs", // secs (number) lastOK: "hq.lock.lastAuthAt", // ms epoch autosave: "hq.autosave.on", // "1" | "" webhook: "hq.alert.webhook", // string webhookOn:"hq.alert.on", // "1" | "" theme: "hq.theme", wall: "hq.wall", bright: "hq.brightness", icons: "hq.icons", dockPin: "hq.dockPinned", dockRec: "hq.dockRecent", }; // -------- State -------- const state = { locked: true, theme: localStorage.getItem(K.theme) || "dark", wallpaper: localStorage.getItem(K.wall) || "graphite", brightness: parseFloat(localStorage.getItem(K.bright) || "1"), icons: JSON.parse(localStorage.getItem(K.icons)||"[]"), dockPinned: JSON.parse(localStorage.getItem(K.dockPin)||"[]"), dockRecent: JSON.parse(localStorage.getItem(K.dockRec)||"[]"), apps: new Map(), z: 10, jiggle:false, autosave: localStorage.getItem(K.autosave)==="1", faceOn: localStorage.getItem(K.faceOn)==="1", relockSecs: parseInt(localStorage.getItem(K.relock)||"0",10) || 0, webhookOn: localStorage.getItem(K.webhookOn)==="1", webhook: localStorage.getItem(K.webhook)||"", weatherCache: null, _extraApps:{} }; // -------- Crypto utility -------- async function sha256Hex(str){ const enc = new TextEncoder().encode(str); const buf = await crypto.subtle.digest("SHA-256", enc); return [...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,"0")).join(""); } // -------- UI helpers -------- function toast(msg){ const t=document.createElement("div"); t.className="toast"; t.textContent=msg; $("#toasts").appendChild(t); setTimeout(()=>{ t.style.opacity="0"; setTimeout(()=>t.remove(),220); }, 1600); } function nowMs(){ return Date.now(); } function secs(n){ return n*1000; } // -------- Service Worker -------- (function registerSW(){ if("serviceWorker" in navigator){ window.addEventListener("load", ()=> { const v = document.querySelector('meta[name="hq-sw-version"]')?.getAttribute("content") || ""; navigator.serviceWorker.register("/static/os/pwa/sw.js?v="+encodeURIComponent(v)).catch(()=>{}); }); } })(); // -------- Theme/Wallpaper/Brightness -------- function setTheme(t){ state.theme=t; localStorage.setItem(K.theme, t); document.documentElement.setAttribute( "data-theme", t==="auto" ? (matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light") : t ); } function setWallpaperChoice(name){ state.wallpaper=name; localStorage.setItem(K.wall, name); // CSS packs by stylesheet (for named walls); custom set below const l = $("#wall-css"); if(!l) return; const v = document.querySelector('meta[name="hq-sw-version"]')?.content || "1"; l.href = `/static/os/wallpapers/.css?v=`; } function setCustomWallpaper(dataUrl){ // inline CSS to override background with the photo/video snapshot let style=document.getElementById("custom-wall"); if(!style){ style=document.createElement("style"); style.id="custom-wall"; document.head.appendChild(style); } style.textContent=`body{background:url() center/cover no-repeat fixed;}`; localStorage.setItem(K.wall,"custom"); localStorage.setItem("hq.wall.custom", dataUrl); } setTheme(state.theme); document.body.style.filter=`brightness()`; // Restore custom wallpaper if set if(state.wallpaper==="custom"){ const d=localStorage.getItem("hq.wall.custom"); if(d){ setCustomWallpaper(d); } } else { setWallpaperChoice(state.wallpaper); } // -------- Status bar time + battery sim -------- function tick(){ const d=new Date(); const hh=String(d.getHours()).padStart(2,"0"); const mm=String(d.getMinutes()).padStart(2,"0"); $("#sb-time").textContent=`:`; $("#lk-time") && ($("#lk-time").textContent=`:`); $("#lk-date") && ($("#lk-date").textContent=d.toLocaleDateString(undefined,{weekday:"long",month:"long",day:"numeric"})); } setInterval(tick,1000); tick(); (function battSim(){ const s=()=>{ const p=65+(new Date().getSeconds()%20); $("#sb-batt") && ($("#sb-batt").style.width=p+"%"); }; s(); setInterval(s,1000); })(); // -------- Catalog (Terminal is stock) -------- const CATALOG=[ {id:"files", name:"Files", emoji:"๐Ÿ“"}, {id:"photos", name:"Photos", emoji:"๐Ÿ–ผ๏ธ"}, {id:"camera", name:"Camera", emoji:"๐Ÿ“ท"}, {id:"messages", name:"Messages", emoji:"๐Ÿ’ฌ"}, {id:"reminders", name:"Reminders", emoji:"โœ…"}, {id:"weather", name:"Weather", emoji:"โ›…"}, {id:"calendar", name:"Calendar", emoji:"๐Ÿ“…"}, {id:"notes", name:"Notes", emoji:"๐Ÿ“"}, {id:"mail", name:"Mail", emoji:"โœ‰๏ธ"}, {id:"browser", name:"Browser", emoji:"๐ŸŒ"}, {id:"settings", name:"Settings", emoji:"โš™๏ธ"}, {id:"appstore", name:"App Store", emoji:"๐Ÿ›๏ธ"}, {id:"terminal", name:"Terminal", emoji:"๐Ÿ–ฅ๏ธ"} ]; // Defaults if(state.icons.length===0){ state.icons=[["photos","camera","messages","reminders","calendar","notes","mail","browser"], ["files","weather","settings","appstore","terminal"]]; localStorage.setItem(K.icons,JSON.stringify(state.icons)); } if(state.dockPinned.length===0){ state.dockPinned=["photos","messages","browser","settings"]; localStorage.setItem(K.dockPin,JSON.stringify(state.dockPinned)); } // -------- Desktop builders -------- function appIcon(meta, pageIndex){ const wrap=document.createElement("div"); wrap.className="icon-wrap"; wrap.dataset.app=meta.id; const ico=document.createElement("button"); ico.className="icon"; ico.type="button"; ico.textContent=meta.emoji; ico.setAttribute("aria-label",meta.name); const del=document.createElement("button"); del.className="del"; del.textContent="ร—"; del.title="Remove"; del.onclick=(e)=>{ e.stopPropagation(); const idx=state.icons[pageIndex].indexOf(meta.id); if(idx>-1){ state.icons[pageIndex].splice(idx,1); localStorage.setItem(K.icons,JSON.stringify(state.icons)); renderPages(); toast(` removed`);} }; ico.appendChild(del); const lbl=document.createElement("div"); lbl.className="icon-label"; lbl.textContent=meta.name; wrap.append(ico,lbl); let pressTimer=null; const startPress=()=>{ pressTimer=setTimeout(()=>{ // Jiggle hint $$(".icon-wrap").forEach(w=>w.classList.add("jiggle")); toast("Jiggle mode โ€” tap ร— to remove; Esc to exit"); }, 550); }; const stopPress=()=>{ if(pressTimer){ clearTimeout(pressTimer); pressTimer=null; } }; wrap.addEventListener("mousedown",startPress); wrap.addEventListener("mouseup",stopPress); wrap.addEventListener("mouseleave",stopPress); wrap.addEventListener("touchstart",startPress,{passive:true}); wrap.addEventListener("touchend",stopPress); wrap.onclick=()=>openApp(meta.id,meta.name); return wrap; } function renderDock(){ const dp=$("#dock-pinned"); dp.innerHTML=""; state.dockPinned.forEach(id=>{ const meta=CATALOG.find(x=>x.id===id); if(!meta) return; const b=document.createElement("button"); b.className="icon"; b.textContent=meta.emoji; b.title=meta.name; b.onclick=()=>openApp(meta.id,meta.name); dp.appendChild(b); }); const dr=$("#dock-recent"); dr.innerHTML=""; (state.dockRecent||[]).forEach(id=>{ const meta=CATALOG.find(x=>x.id===id); if(!meta) return; const b=document.createElement("button"); b.className="icon"; b.textContent=meta.emoji; b.title=meta.name; b.onclick=()=>openApp(meta.id,meta.name); dr.appendChild(b); }); } function addRecent(id){ const cur=JSON.parse(localStorage.getItem(K.dockRec)||"[]"); const next=[id,...cur.filter(x=>x!==id)].slice(0,3); localStorage.setItem(K.dockRec,JSON.stringify(next)); state.dockRecent=next; renderDock(); } function renderDots(){ const dots=$("#page-dots"); dots.innerHTML=""; const pages=$$(".page", $("#pages")); pages.forEach((_,i)=>{ const dot=document.createElement("div"); dot.className="dot"+(i===0?" active":""); dots.appendChild(dot); }); $("#pages").addEventListener("scroll",()=>{ const idx=Math.round($("#pages").scrollLeft / $("#pages").clientWidth); $$(".dot",dots).forEach((d,i)=>d.classList.toggle("active",i===idx)); }, {passive:true}); } function renderPages(){ const pages=$("#pages"); pages.innerHTML=""; state.icons.forEach((row,i)=>{ const page=document.createElement("div"); page.className="page"; page.dataset.index=i; row.forEach(id=>{ const meta=CATALOG.find(m=>m.id===id); if(meta) page.appendChild(appIcon(meta,i)); }); pages.appendChild(page); }); renderDock(); renderDots(); } // -------- Windows & Switcher -------- function makeWindow(id,title){ const w=document.createElement("div"); w.className="win active"; w.dataset.app=id; w.style.zIndex=++state.z; `; $("#applayer").appendChild(w); w.querySelector("[data-close]").onclick=()=>{ closeApp(id); }; w.addEventListener("mousedown",()=>focusWin(w)); state.apps.set(id,{open:true,node:w}); return w; } function focusWin(w){ $$(".win").forEach(x=>x.classList.remove("active")); w.classList.add("active"); w.style.zIndex=++state.z; } function closeApp(id){ const rec=state.apps.get(id); if(!rec) return; rec.node.remove(); state.apps.delete(id); } function openApp(id,title){ // Relock enforcement: if relock elapsed, force auth if(shouldRelock()){ lockScreen(); return; } if(state.locked){ unlockFlow(); return; } addRecent(id); const exists=state.apps.get(id); if(exists){ focusWin(exists.node); return; } const w=makeWindow(id,title); const sel=`#body-`; if(state._extraApps[id]){ state._extraApps[id](sel); return; } if(id==="notes") appNotes(sel); else if(id==="files") appFiles(sel); else if(id==="settings") appSettings(sel); else if(id==="calendar") appCalendar(sel); else if(id==="mail") appMail(sel); else if(id==="browser") appBrowser(sel); else if(id==="camera") appCamera(sel); else if(id==="photos") appPhotos(sel); else if(id==="messages") appMessages(sel); else if(id==="reminders") appReminders(sel); else if(id==="weather") appWeather(sel); else if(id==="appstore") appStore(sel); else if(id==="terminal") appTerminal(sel); else $(sel).innerHTML="
Unknown app.
"; } // Switcher MVP function renderSwitcherCards(){ const sw=$("#switcher"); sw.innerHTML=""; state.apps.forEach((rec,id)=>{ const c=document.createElement("div"); c.className="card"; c.textContent=id; c.onclick=()=>{ focusWin(rec.node); toggleSwitcher(false); }; sw.appendChild(c); }); } function toggleSwitcher(on){ $("#switcher").classList.toggle("active", !!on); $("#switcher").setAttribute("aria-hidden", on?"false":"true"); } // -------- CC / NC / Hotkeys / Gestures -------- function toggleCC(force){ const on = force!==undefined ? force : !$("#cc").classList.contains("active"); $("#cc").classList.toggle("active", on); } function toggleNC(force){ const on = force!==undefined ? force : !$("#nc").classList.contains("active"); $("#nc").classList.toggle("active", on); } document.addEventListener("keydown",(e)=>{ if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="q") toggleCC(); if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="n") toggleNC(); if(e.key==="Escape"){ toggleCC(false); toggleNC(false); toggleSwitcher(false); hideSpot(); $$(".icon-wrap").forEach(w=>w.classList.remove("jiggle")); } if(e.key===" " && !state.locked){ showSpot(); e.preventDefault(); } if(e.key==="Enter" && state.locked){ unlockFlow(); } }); (function gestureTopPull(){ let pulling=false, fromRight=false, startY=0; const nearTop=(y)=> y<50; const right=(x)=> x>window.innerWidth*0.66; document.addEventListener("touchstart",e=>{ const t=e.touches[0]; if(nearTop(t.clientY)){ pulling=true; fromRight=right(t.clientX); startY=t.clientY; } }, {passive:true}); document.addEventListener("touchmove",e=>{ if(!pulling) return; const t=e.touches[0], dy=t.clientY-startY; if(dy>28){ if(fromRight) toggleCC(true); else toggleNC(true); pulling=false; } }, {passive:true}); document.addEventListener("touchend",()=>{ pulling=false; }); })(); // CC sliders $("#cc-bright")?.addEventListener("input",e=>{ const v=parseFloat(e.target.value); state.brightness=v; localStorage.setItem(K.bright,String(v)); document.body.style.filter=`brightness()`; }); // -------- Spotlight -------- function showSpot(){ $("#spotlight").classList.add("active"); $("#spot-input").focus(); renderSpot(""); } function hideSpot(){ $("#spotlight").classList.remove("active"); } $("#spot-input")?.addEventListener("input", e=>renderSpot(e.target.value)); function renderSpot(q){ q=(q||"").toLowerCase(); const box=$("#spot-results"); box.innerHTML=""; const items=[]; // apps CATALOG.forEach(a=>{ if(a.name.toLowerCase().includes(q)) items.push({label:a.name, act:()=>openApp(a.id,a.name)}); }); // settings sections [{label:"Appearance โ€” Settings",sec:"appearance"}, {label:"Security โ€” Settings",sec:"security"}, {label:"System โ€” Settings",sec:"system"}, {label:"About โ€” Settings",sec:"about"}] .forEach(s=>{ if(s.label.toLowerCase().includes(q)) items.push({label:s.label, act:()=>{openApp("settings","Settings"); setTimeout(()=>document.querySelector(\`.set-item[data-sec=""]\`)?.click(),0); }}); }); // notes try{ const notes=JSON.parse(localStorage.getItem("hq.notes")||"[]"); notes.forEach(n=>{ if((n.t||"").toLowerCase().includes(q)) items.push({label:` โ€” Note`, act:()=>openApp("notes","Notes")}); }); }catch{} // files try{ const files=JSON.parse(localStorage.getItem("hq.files")||"[]"); files.forEach(f=>{ if((f.name||"").toLowerCase().includes(q)) items.push({label:` โ€” File`, act:()=>openApp("files","Files")}); }); }catch{} items.forEach(it=>{ const row=document.createElement("div"); row.className="spot-item"; row.textContent=it.label; row.onclick=()=>{ it.act(); hideSpot(); }; box.appendChild(row); }); } // -------- Lock / Security -------- function setLastOK(){ localStorage.setItem(K.lastOK, String(nowMs())); } function getLastOK(){ return parseInt(localStorage.getItem(K.lastOK)||"0",10); } function shouldRelock(){ if(!state.relockSecs) return false; const last=getLastOK(); if(!last) return true; return (nowMs()-last) > secs(state.relockSecs); } function lockScreen(){ state.locked=true; $("#homescreen").classList.remove("active"); $("#lockscreen").classList.add("active"); } async function unlockFlow(){ // If passcode not set, run first setup if(!localStorage.getItem(K.passcode)) return await firstSetupPasscode(); // If Face ID enabled, try quick if(state.faceOn){ const ok = await tryFaceAuth(); await snapshotAttempt(ok); if(ok){ state.locked=false; setLastOK(); uiUnlocked(); toast("Unlocked (Face ID)"); return; } // fallthrough to passcode } const pass = await promptPasscode("Enter Passcode"); if(!pass) return; const ok = await verifyPass(pass); await snapshotAttempt(ok); if(ok){ state.locked=false; setLastOK(); uiUnlocked(); toast("Unlocked"); } else toast("Incorrect passcode"); } function uiUnlocked(){ $("#lockscreen").classList.remove("active"); $("#homescreen").classList.add("active"); renderPages(); } async function firstSetupPasscode(){ const newCode = await promptPasscode("Create Passcode"); if(!newCode) return; const hash = await sha256Hex(newCode); localStorage.setItem(K.passcode, hash); await setupRecoveryQA(); toast("Passcode set"); const enable = await confirmDialog("Enable Face ID?", "Weโ€™ll store a local camera snapshot to help unlock."); if(enable) await enrollFaceTemplate(); state.locked=false; setLastOK(); uiUnlocked(); } async function verifyPass(pass){ const want = localStorage.getItem(K.passcode)||""; if(!want) return false; const got = await sha256Hex(pass); return got===want; } // Modals function modalFromTemplate(tplId){ const tpl = $(tplId); const frag = tpl.content.cloneNode(true); document.body.appendChild(frag); const overlay = document.body.lastElementChild; return { overlay, destroy:()=>overlay.remove() }; } async function promptPasscode(title){ const m = modalFromTemplate("#tpl-passcode-modal"); $(".modal-title", m.overlay).textContent = title; const input = $("#pc-in",m.overlay); const okBtn = $("#pc-ok",m.overlay); const cancel = $("#pc-cancel",m.overlay); const forgot = $("#pc-forgot",m.overlay); input.focus(); return await new Promise(res=>{ cancel.onclick=()=>{ m.destroy(); res(null); }; okBtn.onclick=()=>{ const v=input.value.trim(); m.destroy(); res(v||null); }; forgot.onclick=async()=>{ m.destroy(); await forgotFlow(); res(null); }; input.addEventListener("keydown",e=>{ if(e.key==="Enter"){ okBtn.click(); } }); }); } async function forgotFlow(){ const q = localStorage.getItem(K.question)||"What is your favorite pet?"; const m = modalFromTemplate("#tpl-forgot-modal"); $("#recov-q",m.overlay).textContent = q; const a = $("#recov-a",m.overlay); const n = $("#recov-new",m.overlay); $("#recov-cancel",m.overlay).onclick=()=> m.destroy(); $("#recov-ok",m.overlay).onclick= async ()=>{ const want = localStorage.getItem(K.answer)||""; const got = await sha256Hex(a.value.trim()); if(!want || got!==want){ toast("Recovery answer incorrect"); return; } if(!n.value.trim()){ toast("Enter new passcode"); return; } const newHash = await sha256Hex(n.value.trim()); localStorage.setItem(K.passcode,newHash); toast("Passcode updated"); m.destroy(); }; } async function setupRecoveryQA(){ const q = prompt("Set recovery question", "What is your favorite pet?"); const a = prompt("Answer (used to reset passcode)", ""); if(!a) return; localStorage.setItem(K.question, q||"What is your favorite pet?"); const ah = await sha256Hex(a.trim()); localStorage.setItem(K.answer, ah); } async function confirmDialog(title,msg){ return window.confirm(`\n\n`); } // Face enroll/auth (naive template similarity) async function enrollFaceTemplate(){ const ok = await confirmDialog("Face ID", "Capture a local face snapshot (single frame) to enroll?"); if(!ok) return false; const stream = await navigator.mediaDevices?.getUserMedia?.({video:true}).catch(()=>null); if(!stream){ toast("Camera unavailable"); return false; } const snap = await captureFrame(stream); localStorage.setItem(K.faceTpl, snap); localStorage.setItem(K.faceOn, "1"); state.faceOn=true; toast("Face ID enrolled"); stream.getTracks().forEach(t=>t.stop()); return true; } async function tryFaceAuth(){ try{ const tpl = localStorage.getItem(K.faceTpl); if(!tpl) return false; const stream = await navigator.mediaDevices?.getUserMedia?.({video:true}).catch(()=>null); if(!stream) return false; const snap = await captureFrame(stream); stream.getTracks().forEach(t=>t.stop()); const ok = similarityScore(tpl,snap) > 0.85; return ok; }catch{ return false; } } function similarityScore(a,b){ if(!a||!b) return 0; const L=Math.max(a.length,b.length); let same=0, M=Math.min(a.length,b.length); for(let i=0;inull); if(!stream) return; const img = await captureFrame(stream); stream.getTracks().forEach(t=>t.stop()); fetch("/api/snapshot",{ method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ data:img, success:Boolean(success), ts: Date.now() }) }).catch(()=>{}); if(state.webhookOn && state.webhook){ fetch("/api/alert",{ method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ url:state.webhook, text:`Unlock @ ` }) }).catch(()=>{}); } }catch{} } // Autosave heartbeat setInterval(()=>{ if(!state.autosave) return; localStorage.setItem("hq.autosave.hb", String(Date.now())); }, 5000); // -------- Apps -------- function appNotes(sel){ $(sel).innerHTML=`
`; const LS="hq.notes"; const arr=JSON.parse(localStorage.getItem(LS)||"[]"); $("#n-save").onclick=()=>{ arr.unshift({t:$("#n-title").value,b:$("#n-body").value,ts:Date.now()}); localStorage.setItem(LS,JSON.stringify(arr)); toast("Saved"); }; } function appFiles(sel){ $(sel).innerHTML=`
`; const LS="hq.files"; let items=JSON.parse(localStorage.getItem(LS)||"[]"); const list=$("#f-list"); function draw(){ list.innerHTML=items.map(f=>`
KB
`).join(""); } $("#f-up").addEventListener("change", async e=>{ const files=[...e.target.files]; for(const f of files){ await f.arrayBuffer(); items.push({name:f.name,size:f.size,ts:Date.now()}); } localStorage.setItem(LS,JSON.stringify(items)); draw(); toast("Imported "+files.length); }); draw(); } function appSettings(sel){ const curQ = localStorage.getItem(K.question) || "What is your favorite pet?"; $(sel).innerHTML=`
`; const body=$("#set-body"); function secAppearance(){ body.innerHTML=`

Appearance

Pick from Photos:
`; $("#s-theme").value=state.theme; $("#s-wall").value=state.wallpaper; $("#s-theme").onchange=(e)=>{ setTheme(e.target.value); toast("Theme updated"); }; $("#s-wall").onchange=(e)=>{ const v=e.target.value; if(v==="custom"){ openApp("photos","Photos"); toast("Tap a photo โ†’ Set as Wallpaper"); } else { setWallpaperChoice(v); toast("Wallpaper updated"); } }; $("#s-pick-wall").onclick=()=>{ openApp("photos","Photos"); toast("Tap a photo โ†’ Set as Wallpaper"); }; } function secSecurity(){ body.innerHTML=`

Security

Passcode
Face ID (experimental; local only)
Relock Timeout
Relock after inactivity:
Alerts (server relay)
`; $("#sec-set-pass").onclick=async()=>{ const pc = await promptPasscode("Enter new passcode"); if(!pc) return; const h = await sha256Hex(pc); localStorage.setItem(K.passcode,h); toast("Passcode saved"); }; $("#sec-recover").onclick=async()=>{ await setupRecoveryQA(); toast("Recovery saved"); }; $("#sec-face-on").checked=state.faceOn; $("#sec-face-on").onchange=(e)=>{ state.faceOn=e.target.checked; localStorage.setItem(K.faceOn, state.faceOn?"1":""); toast("Face ID "+(state.faceOn?"enabled":"disabled")); }; $("#sec-enroll-face").onclick=()=>enrollFaceTemplate(); $("#sec-relock").value=String(state.relockSecs||0); $("#sec-relock").onchange=(e)=>{ state.relockSecs=parseInt(e.target.value,10)||0; localStorage.setItem(K.relock, String(state.relockSecs)); toast("Relock updated"); }; $("#sec-alert-on").checked=state.webhookOn; $("#sec-alert-url").value=state.webhook||""; $("#sec-alert-on").onchange=(e)=>{ state.webhookOn = e.target.checked; localStorage.setItem(K.webhookOn, state.webhookOn?"1":""); }; $("#sec-alert-url").onchange=(e)=>{ state.webhook = e.target.value.trim(); localStorage.setItem(K.webhook, state.webhook); }; } function secSystem(){ body.innerHTML=`

System

Brightness
`; $("#s-br").oninput=(e)=>{ const v=parseFloat(e.target.value); state.brightness=v; localStorage.setItem(K.bright,String(v)); document.body.style.filter=`brightness()`; }; $("#s-autosave").checked=state.autosave; $("#s-autosave").onchange=(e)=>{ state.autosave=e.target.checked; localStorage.setItem(K.autosave, state.autosave?"1":""); toast("Autosave "+(state.autosave?"on":"off")); }; } function secAbout(){ const sw = document.querySelector('meta[name="hq-sw-version"]')?.getAttribute("content") || "n/a"; body.innerHTML=`

About

HQ โ€” iPadOS-style shell (PWA + Node)
SW:
UA:
Viewport: ร—
`; } $$(".set-item").forEach(i=>{ const go=()=>({appearance:secAppearance, security:secSecurity, system:secSystem, about:secAbout}[i.dataset.sec]()); i.onclick=go; i.onkeydown=(e)=>{ if(e.key==="Enter"||e.key===" ") go(); }; }); secAppearance(); } function appCalendar(sel){ const d=new Date(); let Y=d.getFullYear(), M=d.getMonth(); function draw(){ const first=new Date(Y,M,1).getDay(), days=new Date(Y,M+1,0).getDate(); let h=`

)}

`; "SMTWTFS".split("").forEach(c=>h+=`
`); for(let i=0;i
`; for(let day=1;day<=days;day++){ const today=(day===d.getDate()&&M===d.getMonth()&&Y===d.getFullYear()); h+=`
`; } h+=``; $(sel).innerHTML=h; $("#cal-prev").onclick=()=>{ M--; if(M<0){M=11;Y--;} draw(); }; $("#cal-next").onclick=()=>{ M++; if(M>11){M=0;Y++;} draw(); }; } draw(); } function appMail(sel){ $(sel).innerHTML=`

Mail

Placeholder

`; } function appBrowser(sel){ $(sel).innerHTML=`
`; $("#go").onclick=()=>{ const u=$("#url").value.trim(); if(!u) return; $("#web").src=u.startsWith("http")?u:("https://"+u); }; } function appCamera(sel){ $(sel).innerHTML=``; navigator.mediaDevices?.getUserMedia({video:true}).then(s=>{ $("#v").srcObject=s; }) .catch(()=>{ $(sel).innerHTML="Camera unavailable (HTTPS or permission required)."; }); } function appPhotos(sel){ $(sel).innerHTML=`
`; const LS="hq.photos"; let items=JSON.parse(localStorage.getItem(LS)||"[]"), grid=$("#grid"); function draw(){ grid.innerHTML=""; items.forEach((it,i)=>{ const isVid = it.data.startsWith("data:video"); const el = isVid?document.createElement("video"):new Image(); el.src=it.data; if(isVid) el.controls=true; el.style.width="100%"; el.onclick=()=>preview(i); grid.appendChild(el); }); } function preview(i){ const m=document.createElement("div"); m.className="modal-overlay"; const inner=document.createElement("div"); inner.className="modal"; inner.style.maxWidth="90vw"; inner.style.background="#000"; const it=items[i]; const isVid=it.data.startsWith("data:video"); const el=isVid?document.createElement("video"):new Image(); el.src=it.data; if(isVid){ el.controls=true; el.autoplay=true; } el.style.maxWidth="82vw"; el.style.maxHeight="82vh"; const row=document.createElement("div"); row.className="modal-row"; const setBtn=document.createElement("button"); setBtn.className="accent"; setBtn.textContent="Set as Wallpaper"; setBtn.onclick=()=>{ setCustomWallpaper(it.data); toast("Wallpaper set"); }; const close=document.createElement("button"); close.textContent="Close"; close.onclick=()=>m.remove(); row.append(close,setBtn); inner.append(el,row); m.append(inner); document.body.appendChild(m); } $("#ph").addEventListener("change", async e=>{ const files=[...e.target.files]; for(const f of files){ const d=await new Promise((res,rej)=>{ const r=new FileReader(); r.onload=()=>res(r.result); r.onerror=rej; r.readAsDataURL(f); }); items.unshift({ts:Date.now(),data:d}); } localStorage.setItem(LS,JSON.stringify(items)); draw(); toast("Imported "+files.length); }); draw(); } function appMessages(sel){ $(sel).innerHTML=`

Messages

Local-only demo

`; } function appReminders(sel){ const root=$(sel); const LS="hq.reminders"; let items=JSON.parse(localStorage.getItem(LS)||"[]"); root.innerHTML=`
`; function draw(){ const ul=$("#rem-list"); ul.innerHTML=""; items.forEach((it,i)=>{ const row=document.createElement("div"); row.style.cssText="display:flex;gap:8px;align-items:center;border:1px solid var(--line);border-radius:10px;padding:8px;background:#0f1219"; row.innerHTML=`
`:""}
`; ul.appendChild(row); }); ul.querySelectorAll("input[type=checkbox]").forEach(c=>c.onchange=()=>{ items[c.dataset.i].done=c.checked; localStorage.setItem(LS,JSON.stringify(items)); draw(); }); ul.querySelectorAll("button[data-del]").forEach(b=>b.onclick=()=>{ items.splice(+b.dataset.del,1); localStorage.setItem(LS,JSON.stringify(items)); draw(); }); } $("#rem-add").onclick=()=>{ const t=$("#rem-text").value.trim(); if(!t) return; items.unshift({text:t,due:$("#rem-date").value||"",done:false}); localStorage.setItem(LS,JSON.stringify(items)); draw(); }; draw(); } // Weather const WX_CODE={0:"Clear",1:"Mainly clear",2:"Partly cloudy",3:"Overcast",61:"Rain",71:"Snow",95:"Thunderstorm"}; async function fetchWeather(lat,lon){ const r=await fetch(`https://api.open-meteo.com/v1/forecast?latitude=&longitude=&current_weather=true&temperature_unit=fahrenheit`); if(!r.ok) throw 0; return r.json(); } async function initWeather(){ try{ const p=await new Promise((res,rej)=>{ if(!navigator.geolocation) return rej(); navigator.geolocation.getCurrentPosition(ps=>res({lat:ps.coords.latitude,lon:ps.coords.longitude}),()=>rej(),{timeout:4000}); }); state.weatherCache=await fetchWeather(p.lat,p.lon); }catch{ state.weatherCache=await fetchWeather(40.7128,-74.0060); } } function renderWeatherWidget(){ const w=state.weatherCache?.current_weather; if(!w){ $("#w-weather").textContent="Weather unavailable"; return; } const label=WX_CODE[w.weathercode]||`Code `; $("#w-weather").innerHTML=`
Weather
ยฐF โ€ข
`; } function appWeather(sel){ $(sel).innerHTML=`

Weather

Loadingโ€ฆ
`; const box=$("#wx"); const paint=()=>{ const w=state.weatherCache?.current_weather; if(!w){ box.textContent="Weather unavailable"; return; } const label=WX_CODE[w.weathercode]||`Code `; box.innerHTML=`
ยฐF
โ€” Wind mph
`; }; if(state.weatherCache?.current_weather){ paint(); } else { initWeather().then(()=>{ paint(); renderWeatherWidget(); }).catch(()=>{ box.textContent="Weather unavailable"; }); } } // Terminal (stock) โ€” WS /pty function ensureXterm(ready){ if(window.Terminal && window.FitAddon) return ready(); const load=(src)=> new Promise((res,rej)=>{ const s=document.createElement("script"); s.src=src; s.onload=res; s.onerror=rej; document.head.appendChild(s); }); const css=document.createElement("link"); css.rel="stylesheet"; css.href="https://unpkg.com/xterm/css/xterm.css"; document.head.appendChild(css); load("https://unpkg.com/xterm/lib/xterm.js").then(()=>load("https://unpkg.com/xterm-addon-fit/lib/xterm-addon-fit.js")).then(ready).catch(()=>toast("Failed to load terminal UI")); } function appTerminal(sel){ $(sel).innerHTML=`
Terminal
Connectingโ€ฆ
`; ensureXterm(()=>{ const term=new window.Terminal({cursorBlink:true,fontFamily:"ui-monospace,Menlo,Consolas,monospace",fontSize:14}); const fit =new window.FitAddon.FitAddon(); term.loadAddon(fit); term.open($("#term-wrap")); fit.fit(); const wsURL=(location.origin.replace(/^http/,'ws'))+"/pty"; let ws; try{ ws=new WebSocket(wsURL); }catch{} const status=$("#term-status"); const setS=(m)=>status.textContent=m; if(!ws){ setS("No WebSocket"); return; } ws.onopen=()=>{ setS("Connected"); const resize=()=>{ fit.fit(); ws.send(JSON.stringify({type:"resize",cols:term.cols,rows:term.rows})); }; window.addEventListener("resize",resize); resize(); term.onData(d=>ws.send(JSON.stringify({type:"data",data:d}))); }; ws.onmessage=(ev)=>{ try{ const m=JSON.parse(ev.data); if(m.type==="data") term.write(m.data); if(m.type==="exit") setS(`Exited ()`);}catch{ term.write(ev.data);} }; ws.onclose=()=>setS("Disconnected"); ws.onerror=()=>setS("Error"); }); } // App Store (extras; Terminal is stock) function appStore(sel){ const root=$(sel); const catalog=[ {id:"calculator",name:"Calculator",emoji:"๐Ÿงฎ"}, {id:"clock",name:"Clock",emoji:"โฐ"} ]; root.innerHTML=`

HQ App Store

`; const grid=$("#apps"); catalog.forEach(a=>{ const card=document.createElement("div"); card.style.cssText="border:1px solid var(--line);border-radius:12px;padding:12px;background:#0f1219"; card.innerHTML=`
`; grid.appendChild(card); }); grid.querySelectorAll("button[data-add]").forEach(b=>b.onclick=()=>{ const id=b.dataset.add; if(!state.icons[0].includes(id)){ state.icons[0].push(id); localStorage.setItem(K.icons,JSON.stringify(state.icons)); toast("Added to Home"); } else toast("Already on Home"); }); // extras impl state._extraApps.calculator=(sel)=>{ const r=$(sel); r.innerHTML=`
`; const d=$("#calc-disp"), keys=$("#calc-keys"); "789/456*123-0.=+".split("").forEach(ch=>{ const b=document.createElement("button"); b.textContent=ch; b.style.cssText="padding:12px;font-size:18px;background:#1a1d25;color:#fff;border:none;border-radius:8px"; b.onclick=()=>{ if(ch==="="){ try{ d.value=String(eval(d.value||"0")||"0"); }catch{ d.value="Err"; } } else d.value+=ch; }; keys.appendChild(b); }); }; state._extraApps.clock=(sel)=>{ const r=$(sel); r.innerHTML=`
`; const box=$("#clk"); const t=()=>box.textContent=new Date().toLocaleTimeString(); t(); setInterval(t,1000); }; } // Today widgets init (function initToday(){ const d=new Date(); $("#w-cal").innerHTML = `
Today
)}
`; try{ const rem=JSON.parse(localStorage.getItem("hq.reminders")||"[]"); const open=rem.filter(r=>!r.done).length; $("#w-rem").innerHTML = `
Reminders
open
`; }catch{ $("#w-rem").textContent="Reminders"; } initWeather().then(renderWeatherWidget).catch(()=>{ $("#w-weather").textContent="Weather unavailable"; }); })(); // Initial lock visible $("#lockscreen").classList.add("active"); $("#lockscreen").addEventListener("click", ()=> unlockFlow()); // Build homescreen after unlock, not before (done in uiUnlocked) })(); # ---------------- Node backend (Express + WS PTY + APIs) ---------------- info "โœ๏ธ Writing Node backend" mkdir -p "" { "name": "hq-node", "version": "1.0.0", "private": true, "main": "server.js", "type": "module", "dependencies": { "express": "^4.19.2", "node-pty": "^1.0.0", "ws": "^8.17.0", "body-parser": "^1.20.2", "fs-extra": "^11.2.0", "node-fetch": "^3.3.2" }, "engines": { "node": ">=16" } } import express from "express"; import path from "path"; import { fileURLToPath } from "url"; import http from "http"; import { WebSocketServer } from "ws"; import os from "os"; import pty from "node-pty"; import bodyParser from "body-parser"; import fs from "fs-extra"; import fetch from "node-fetch"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PORT = Number(process.env.HQ_PORT || "8080"); const HOST = process.env.HQ_HOST || "127.0.0.1"; const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server, path: "/pty" }); // Directories const STATIC_DIR = path.join(__dirname, "..", "..", "static"); const SNAP_DIR = path.join(__dirname, "..", "..", "assets", "snapshots"); fs.ensureDirSync(SNAP_DIR); // Middleware app.use("/static", express.static(STATIC_DIR)); app.use(bodyParser.json({limit:"5mb"})); // Root redirect app.get("/", (req,res)=> res.redirect("/static/os/index.html")); // -------- PTY WebSocket -------- function pickShell() { if (process.env.SHELL) return process.env.SHELL; if (process.platform==="darwin") return "/bin/zsh"; if (process.platform==="linux") return "/bin/bash"; return "/bin/sh"; } wss.on("connection",(ws)=>{ const shell = pickShell(); const sh = pty.spawn(shell, [], { name:"xterm-color", cols:120, rows:30, cwd: process.env.HOME || os.homedir(), env: process.env }); sh.onData(d=>{ try{ ws.send(JSON.stringify({type:"data",data:d})); }catch{} }); sh.onExit(e=>{ try{ ws.send(JSON.stringify({type:"exit",code:e.exitCode})); ws.close(); }catch{} }); ws.on("message",(buf)=>{ try{ const msg = JSON.parse(buf.toString()); if(msg.type==="data") sh.write(msg.data); if(msg.type==="resize" && msg.cols && msg.rows) sh.resize(msg.cols,msg.rows); }catch{ sh.write(buf.toString()); } }); ws.on("close",()=>sh.kill()); }); // -------- API: snapshot -------- app.post("/api/snapshot", async (req,res)=>{ try{ const {data,success,ts} = req.body; if(!data) return res.status(400).json({error:"no data"}); const fname = path.join(SNAP_DIR, `-.jpg`); const base64 = data.split(",")[1]; await fs.writeFile(fname, Buffer.from(base64,"base64")); return res.json({ok:true,file:path.basename(fname)}); }catch(e){ console.error("snapshot err",e); res.status(500).json({error:"fail"}); } }); // -------- API: alert relay -------- app.post("/api/alert", async (req,res)=>{ try{ const {url,text} = req.body; if(!url) return res.status(400).json({error:"no url"}); await fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text})}); res.json({ok:true}); }catch(e){ console.error("alert err",e); res.status(500).json({error:"fail"}); } }); server.listen(PORT, HOST, ()=>{ console.log(`HQ backend listening on http://:`); console.log(`Serving static from `); console.log(`Snapshots saved in `); }); # ---------------- Backend Package ---------------- info "โœ๏ธ Writing backend package.json" { "name": "hq-node", "version": "1.0.0", "private": true, "main": "server.js", "type": "module", "dependencies": { "express": "^4.19.2", "node-pty": "^1.0.0", "ws": "^8.17.0", "body-parser": "^1.20.2", "fs-extra": "^11.2.0", "node-fetch": "^3.3.2" }, "engines": { "node": ">=16" } } # ---------------- Install Node deps ---------------- info "๐Ÿ“ฆ Installing backend dependencies" ( cd "" && npm install --production ) || { err "npm install failed"; exit 1; } # ---------------- Permissions ---------------- info "๐Ÿ”“ Setting open permissions on " chmod -R "" "" || true # ---------------- Persistent Service ---------------- if [[ "$OS_FAMILY" == "macos" ]]; then info "โš™๏ธ Installing LaunchDaemon (Node backend)" chmod 644 "" chown root:wheel "" launchctl bootout system "" 2>/dev/null || true launchctl bootstrap system "" || launchctl load -w "" launchctl enable "system/" || true launchctl kickstart -k "system/" || true else info "โš™๏ธ Installing systemd unit (Linux)" [Unit] Description=HQ Node backend (static + PTY + APIs) After=network.target [Service] Type=simple User= WorkingDirectory= Environment=HQ_PORT= Environment=HQ_HOST= ExecStart=$(command -v node) /server.js Restart=always RestartSec=2 [Install] WantedBy=multi-user.target chmod 644 "" systemctl daemon-reload || true systemctl enable --now hq-node.service || systemctl start hq-node.service || true fi # ---------------- Final notes / self test ---------------- echo info "๐Ÿ”Ž Testing deployment (curl HEAD http://:/static/os/index.html)" if command -v curl >/dev/null 2>&1; then set +e CODE=$(curl -sI "http://:/static/os/index.html" | awk 'NR==1{print $2}') set -e echo "HTTP status -> " else info "curl not present, skipping HEAD test." fi echo echo "โœ… HQ deployed (Node-only full MVP)." echo echo "Web UI:" echo " http://:/static/os/index.html" echo " http://hq.local:/static/os/index.html" echo echo "Terminal app:" echo " Connects to WS at ws://:/pty" echo echo "Lock Screen:" echo " - Password required (set in Settings > Security)" echo " - Failed attempts trigger camera snapshot (saved in /opt/hq/assets/snapshots)" echo " - Optional facial recognition (experimental)" echo echo "Persistence:" echo " - macOS: LaunchDaemon " echo " - Linux: systemd service hq-node.service" echo echo "Check service status:" echo " macOS -> sudo launchctl list | grep " echo " Linux -> systemctl status hq-node.service" echo # ---------------- Wallpapers pack + README ---------------- info "โœ๏ธ Writing wallpaper CSS pack and README" # NOTE: if earlier WPS had a stray '}' (WPS="/wallpapers}") # this will still work, as we write to a computed path independent of WPS. WPS_PATH="/wallpapers" mkdir -p "" # Wallpaper variants (referenced by Settings > Appearance) cat > "/graphite.css" <<'CSS' /* Graphite */ body{ background: radial-gradient(120% 140% at 50% -20%, #12141b 0%, #0b0d14 55%, #07090d 100%); color: #e6e7eb; } cat > "/midnight.css" <<'CSS' /* Midnight */ body{ background: radial-gradient(120% 150% at 50% -20%, #0b1022, #0a0d19 55%, #070a13 100%); color: #e6e7eb; } cat > "/sand.css" <<'CSS' /* Sand (light) */ body{ background: radial-gradient(120% 150% at 50% -20%, #f3eee4, #e5dccb 55%, #dbcfbb 100%); color: #1a1d25; } cat > "/ocean.css" <<'CSS' /* Ocean */ body{ background: radial-gradient(120% 150% at 50% -20%, #0e2f3e 0%, #09232e 55%, #071c27 100%); color: #e6e7eb; } # Small project README for quick ops cat > "/README-HQ.txt" < "/package.json" <=16" } } cat > "/server.js" <<'JS' import express from "express"; import path from "path"; import { fileURLToPath } from "url"; import http from "http"; import { WebSocketServer } from "ws"; import os from "os"; import pty from "node-pty"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PORT = Number(process.env.HQ_PORT || "8080"); const HOST = process.env.HQ_HOST || "127.0.0.1"; const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server, path: "/pty" }); // static dir (frontend assets) const STATIC_DIR = path.join(__dirname, "..", "..", "static"); app.use("/static", express.static(STATIC_DIR)); app.get("/", (req,res)=> res.redirect("/static/os/index.html")); // PTY for Terminal app function pickShell() { if (process.env.SHELL) return process.env.SHELL; if (process.platform==="darwin") return "/bin/zsh"; if (process.platform==="linux") return "/bin/bash"; return "/bin/sh"; } wss.on("connection",(ws)=>{ const shell = pickShell(); const sh = pty.spawn(shell, [], { name:"xterm-color", cols:120, rows:30, cwd: process.env.HOME || os.homedir(), env: process.env }); sh.onData(d=>ws.send(JSON.stringify({type:"data",data:d}))); sh.onExit(e=>{ ws.send(JSON.stringify({type:"exit",code:e.exitCode})); ws.close(); }); ws.on("message",(buf)=>{ try { const msg = JSON.parse(buf.toString()); if(msg.type==="data") sh.write(msg.data); if(msg.type==="resize") sh.resize(msg.cols,msg.rows); } catch { sh.write(buf.toString()); } }); ws.on("close",()=>sh.kill()); }); server.listen(PORT, HOST, ()=>{ console.log(`HQ backend listening on http://:`); console.log(`Serving static from `); }); # ---------------- Install Node deps ---------------- info "๐Ÿ“ฆ Installing backend dependencies" info "โœ๏ธ Writing Node backend" mkdir -p "" import express from "express"; import path from "path"; import { fileURLToPath } from "url"; import http from "http"; import { WebSocketServer } from "ws"; import os from "os"; import pty from "node-pty"; import bodyParser from "body-parser"; import fs from "fs-extra"; import fetch from "node-fetch"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PORT = Number(process.env.HQ_PORT || "8080"); const HOST = process.env.HQ_HOST || "127.0.0.1"; const server = http.createServer(app); const wss = new WebSocketServer({ server, path: "/pty" }); const STATIC_DIR = path.join(__dirname, "..", "..", "static"); const SNAP_DIR = path.join(__dirname, "..", "..", "assets", "snapshots"); fs.ensureDirSync(SNAP_DIR); app.use("/static", express.static(STATIC_DIR)); app.use(bodyParser.json({limit:"5mb"})); app.get("/", (req,res)=> res.redirect("/static/os/index.html")); function pickShell() { if (process.env.SHELL) return process.env.SHELL; if (process.platform==="darwin") return "/bin/zsh"; if (process.platform==="linux") return "/bin/bash"; return "/bin/sh"; } wss.on("connection",(ws)=>{ const shell = pickShell(); const sh = pty.spawn(shell, [], { name:"xterm-color", cols:120, rows:30, cwd: process.env.HOME || os.homedir(), env: process.env }); sh.onData(d=>{ try{ ws.send(JSON.stringify({type:"data",data:d})); }catch{} }); sh.onExit(e=>{ try{ ws.send(JSON.stringify({type:"exit",code:e.exitCode})); ws.close(); }catch{} }); ws.on("message",(buf)=>{ try{ const msg = JSON.parse(buf.toString()); if(msg.type==="data") sh.write(msg.data); if(msg.type==="resize" && msg.cols && msg.rows) sh.resize(msg.cols,msg.rows); }catch{ sh.write(buf.toString()); } }); ws.on("close",()=>sh.kill()); }); app.post("/api/snapshot", async (req,res)=>{ try{ const {data,success,ts} = req.body; if(!data) return res.status(400).json({error:"no data"}); const fname = path.join(SNAP_DIR, `-.jpg`); const base64 = data.split(",")[1]; await fs.writeFile(fname, Buffer.from(base64,"base64")); return res.json({ok:true,file:path.basename(fname)}); }catch(e){ console.error("snapshot err",e); res.status(500).json({error:"fail"}); } }); app.post("/api/alert", async (req,res)=>{ try{ const {url,text} = req.body; if(!url) return res.status(400).json({error:"no url"}); await fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text})}); res.json({ok:true}); }catch(e){ console.error("alert err",e); res.status(500).json({error:"fail"}); } }); server.listen(PORT, HOST, ()=>{ console.log(\`HQ backend listening on http://:\`); console.log(\`Serving static from \`); console.log(\`Snapshots saved in \`); }); ( cd "" && npm install --production ) || { err "npm install failed"; exit 1; }