Techect

Al alcance de un clic — Ingresa el identificador y secreto del dispositivo

Techect

Trackpad Inmersivo
Gamepad Inmersivo
Estado de la PC
OFF
Cambio:
Reinicio:
ESP32 Activo
Latencia
RAM Libre
Señal WiFi

Al alcance de un clic

Encendido por LAN
Dispositivos
Tareas programadas
Nueva rutina
Bluetooth HID: Sin conexión
Trackpad
Sensibilidad 0.5
1 dedo: clic izq · 2 dedos: clic der · 3 dedos: clic medio
Teclado
Modificadores
Funciones
Navegación
Gamepad
Control remoto en la nube

Con techect.pages.dev tienes acceso básico desde cualquier lugar, sin configuración: encendido y apagado de tu PC, wake-on-LAN, estado en tiempo real, rutinas y notificaciones.

Para el control completo (trackpad, teclado, gamepad y conexión BLE) accede directamente a tu red local con una VPN:

  1. Instala Tailscale en tus dispositivos
  2. Configura subnet routing o un exit node para alcanzar toda tu red
  3. Accede a http://[IP_DEL_DISPOSITIVO]/ desde tu dispositivo remoto

Alternativa: WireGuard para conectarte directamente a tu red local.

Información del dispositivo
Manual
en function escapeHtml(s){if(s==null)return'';return String(s).replace(/[&<>"'\/]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"',"'":''','/':'/'}[c];});} let did='',timer=null,pwState=false,lastDeviceState=null,timezoneOffset=0,_firstLoad=true,rtns=[]; function fmtUptime(ms){ if(!ms||ms<0)return '0m'; const t=Math.floor(ms/1000); const d=Math.floor(t/86400); const h=Math.floor((t%86400)/3600); const m=Math.floor((t%3600)/60); let r=''; if(d)r+=d+'d '; if(h)r+=h+'h '; r+=m+'m'; return r; } function updateClock(){ const now = new Date(); const utc = now.getTime(); const esp = new Date(utc + timezoneOffset * 60000); document.getElementById('clock').textContent = esp.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit', timeZone: 'UTC', hour12: false }); } function t(m){const e=document.getElementById('toast');e.textContent=m;e.classList.add('show');clearTimeout(e._t);e._t=setTimeout(()=>e.classList.remove('show'),2500)} /* ---- Theme ---- */ function toggleTheme(){ const html=document.documentElement; const cur=html.getAttribute('data-theme'); const next=cur==='light'?'dark':'light'; html.setAttribute('data-theme',next); localStorage.setItem('theme',next); updateThemeBtn(); } function updateThemeBtn(){ const theme=document.documentElement.getAttribute('data-theme'); const btn=document.getElementById('themeBtn'); btn.innerHTML=theme==='light' ?'' :''; } function applyTheme(){ const saved=localStorage.getItem('theme')||'dark'; document.documentElement.setAttribute('data-theme',saved); updateThemeBtn(); } /* ---- Navigation ---- */ document.querySelectorAll('.bnv').forEach(b=>{ b.addEventListener('click',()=>{ document.querySelectorAll('.bnv').forEach(x=>x.classList.remove('active')); document.querySelectorAll('.view').forEach(x=>x.classList.remove('active')); b.classList.add('active'); document.querySelector('[data-v="'+b.dataset.v+'"]').classList.add('active'); }); }); function toggleSec(id){ const s=document.getElementById(id); if(s)s.classList.toggle('open'); } /* ---- API ---- */ async function api(path,opts={}){ const s=localStorage.getItem('sec')||'',d=localStorage.getItem('d')||''; const h={'Content-Type':'application/json','X-API-Key':KEY,'X-Device-ID':d,'X-Device-Secret':s,...(opts.headers||{})}; try{ const r=await fetch(API+path,{...opts,headers:h}); if(r.status===401){logout();return null} const json = await r.json().catch(()=>null); if(r.ok) return json; return json||null; }catch(e){return null} } /* ---- Login ---- */ const di=document.getElementById('di'),ds=document.getElementById('ds'),ps=document.getElementById('ps'); di.addEventListener('input',()=>{di.value=di.value.toUpperCase().replace(/[^0-9A-F:]/g,'').slice(0,17)}); di.addEventListener('keydown',e=>{if(e.key==='Enter')ds.focus()}); ds.addEventListener('keydown',e=>{if(e.key==='Enter')authGo()}); function err(m){document.getElementById('pe').textContent=m} async function authGo(){ const id=di.value,sec=ds.value; if(!id||!sec){err('Completa ambos campos');return} ps.disabled=true;di.disabled=true;ds.disabled=true; try{ const r=await fetch(API+'/auth',{method:'POST',headers:{'Content-Type':'application/json','X-API-Key':KEY},body:JSON.stringify({device_id:id,device_secret:sec})}); const d=await r.json(); if(d&&d.ok&&d.device){localStorage.setItem('sec',sec);localStorage.setItem('d',id);did=id;if(!localStorage.getItem('tut_done'))window._showTutorial=true;enterApp();return} }catch(_){} err('Credenciales invalidas');di.value='';ds.value='';di.disabled=false;ds.disabled=false;di.focus();ps.disabled=false; } async function start(){ const ss=localStorage.getItem('sec'),sd=localStorage.getItem('d'); if(ss&&sd){ did=sd; try{const r=await fetch(API+'/device',{headers:{'X-API-Key':KEY,'X-Device-ID':sd,'X-Device-Secret':ss}});if(r.ok){enterApp();return}}catch(_){} localStorage.removeItem('sec');localStorage.removeItem('d');did=''; } } function enterApp(){ document.getElementById('pinO').classList.add('hidden'); document.getElementById('app').style.display='flex'; boot(); } function logout(){ localStorage.removeItem('sec');localStorage.removeItem('d');did=''; if(timer)clearInterval(timer); document.getElementById('app').style.display='none';document.getElementById('pinO').classList.remove('hidden'); di.value='';ds.value='';di.disabled=false;ds.disabled=false;di.focus();ps.disabled=false;err(''); } /* ---- Widgets ---- */ async function loadWidgets(){ const d=lastDeviceState; if(!d)return; document.getElementById('statUptime').textContent=fmtUptime(d.esp_uptime_ms); document.getElementById('statRAM').textContent=(d.free_heap/1024).toFixed(1)+' KB'; document.getElementById('statRSSI').textContent=d.wifi_rssi+' dBm'; const stateTime = d.last_state_change; if(stateTime){ const date = new Date(stateTime * 1000); document.getElementById('statStateTime').textContent = (d.pc_online ? 'Encendida ' : 'Apagada ') + date.toLocaleTimeString('es-ES',{hour:'2-digit',minute:'2-digit'}); } else { document.getElementById('statStateTime').textContent = '—'; } document.getElementById('statLatency').textContent=(d.internet_latency||0)+' ms'; } async function loadDev(){ const d=await api('/device'); if(!d||!d.device) return const dv=d.device; if(_firstLoad){ _firstLoad=false; if(window._showTutorial){setTimeout(function(){showTutorial();updateTut()},600);window._showTutorial=false} } lastDeviceState=dv; var cfg={}; try{cfg=typeof dv.config==='string'?JSON.parse(dv.config):(dv.config||{})}catch(e){} const hostname=cfg.hostname||dv.device_id||'Techect'; const pcHostname = dv.pc_hostname || 'Desconocido'; const ip=cfg.pc_ip||dv.ip||dv.sta_ip||''; const uptimeEl = document.getElementById('hdrUptime'); if (uptimeEl) uptimeEl.textContent = fmtUptime(dv.esp_uptime_ms); const pwSub = document.getElementById('pwSub'); if(pwSub) pwSub.innerHTML = 'Conectado a ' + escapeHtml(pcHostname); var ls=dv.last_seen; if(ls){ var tz=parseTZ(dv.timezone||'UTC-3'); timezoneOffset=tz; updateClock(); var t=new Date(ls.replace(' ','T')+'Z').getTime(); ls=Date.now()-t<70000?'Conectado':fmtUTC(ls); }else{ls='desconectado'} pwState=dv.pc_online?true:false; updatePowerUI(dv); // Reset Reason if(dv.reset_reason) { document.getElementById('resetReason').textContent = dv.reset_reason; } // Next Routine const nextRtnWrap = document.getElementById('nextRtnWrap'); if(dv.next_routine) { nextRtnWrap.style.display = 'flex'; document.getElementById('nextRtn').textContent = `${dv.next_routine.hour.toString().padStart(2,'0')}:${dv.next_routine.minute.toString().padStart(2,'0')} - ${dv.next_routine.action}`; } else { nextRtnWrap.style.display = 'none'; } const model=dv.esp_model||cfg.esp_model||'N/A'; const mac=dv.device_id||'N/A'; document.getElementById('ainfo').innerHTML= '
DispositivoTechect
'+ (model!=='N/A'?'
Modelo'+escapeHtml(model)+'
':'')+ (dv.sta_ip?'
IP del dispositivo'+escapeHtml(dv.sta_ip)+'
':'')+ '
Dirección MAC'+escapeHtml(mac)+'
'+ '
¿Observas que faltan funciones en la página? No te preocupes: la página web ofrece acceso básico. Para aprovechar todas las funciones de tu dispositivo Techect, ingresa a la IP local del dispositivo o a techect-pc-remote.local. Estamos para ti.
'; const vpnEl=document.getElementById('vpnIp'); if(vpnEl&&dv.sta_ip)vpnEl.textContent=dv.sta_ip; var s=function(n){return"'"+n+"'"}; document.getElementById('amanual').innerHTML= '
'+ ''+ '
'+ '

Techect es un dispositivo de control remoto que te permite encender, apagar y monitorear tu PC desde cualquier lugar. Se conecta a tu red WiFi y se comunica con la placa madre de tu PC mediante GPIO.

'+ '
'+ '
'+ '
'+ ''+ '
'+ '

Usa la pestaña Remoto para acceder al trackpad, teclado virtual, controles multimedia y gamepad. El trackpad usa WebSocket de baja latencia para responder en tiempo real.

'+ '

Para control remoto fuera de tu casa, puedes usar programas como Parsec, Chrome Remote Desktop, Moonlight o Sunshine combinados con Tailscale o WireGuard para una conexión segura.

'+ '
'+ '
'+ '
'+ ''+ '
'+ '

Agrega dispositivos en la sección Encendido por LAN con su dirección MAC para encenderlos de forma remota mediante paquetes WOL.

'+ '
'+ '
'; loadWidgets(); } function updatePowerUI(dv){ const el=document.getElementById('pwState'),btn=document.getElementById('pwBtn'),sub=document.getElementById('pwSub'); if(pwState){ el.textContent='ENCENDIDA';el.className='pw-state on'; btn.className='pw-btn on'; btn.innerHTML=''; sub.innerHTML='Activa desde hace '+fmtUptime(dv.pc_uptime_ms); }else{ el.textContent='APAGADA';el.className='pw-state off'; btn.className='pw-btn off'; btn.innerHTML=''; sub.textContent='Dispositivo listo para encendido'; } } async function togglePower(){ const btn=document.getElementById('pwBtn'); btn.className='pw-btn loading off';btn.textContent='...'; await cmd('toggle_pc'); } async function cmd(action){ const d=await api('/command',{method:'POST',body:JSON.stringify({action:action})}); if(d){t('Comando enviado');setTimeout(()=>loadDev(),2000)}else{t('Error de conexión')} } /* ---- Config ---- */ let _loadingCfg=false; function sav(f){ const data={}; new FormData(f).forEach((v,k)=>{ data[k]=v; }); f.querySelectorAll('input[type="checkbox"]').forEach(el=>{ data[el.name]=el.checked; }); api('/device/config').then(cur=>{ const c = cur&&cur.config?cur.config:{}; const merged={...c,...data}; api('/device/config',{method:'POST',body:JSON.stringify(merged)}).then(d=>{ if(d&&d.success){ t('Configuración guardada exitosamente'); loadCfg() }else{t('Error al guardar: '+(d?d.error:'Desconocido'))} }); }); } function saveCfg(e){e.preventDefault();sav(e.target);return false} /* ---- Timezone helpers ---- */ function parseTZ(tz){ if(!tz)return 0; const m=tz.match(/UTC([+-])(\d+)(?::(\d+))?/); if(!m)return 0; let o=parseInt(m[2])*60; if(m[3])o+=parseInt(m[3]); return m[1]==='-'?-o:o; } function fmtUTC(utcStr){ if(!utcStr)return 'N/A'; try { const d = new Date(utcStr.replace(' ', 'T') + 'Z'); if(isNaN(d.getTime())) return utcStr; const local = new Date(d.getTime() + timezoneOffset * 60000); return local.toLocaleString('es-ES', { timeZone: 'UTC', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(',', ''); } catch(e) { return utcStr; } } async function loadCfg(){ const d=await api('/device/config'); if(!d||!d.config)return; const c=d.config; const map=[ ['timezone','timezone'],['pc_ip','pc_ip'],['pc_mac','pc_mac'], ['use_static_ip','use_static_ip'],['static_ip','static_ip'],['static_gateway','static_gateway'],['static_mask','static_mask'], ['power_on_ms','power_on_ms'],['force_off_ms','force_off_ms'],['reset_off_ms','reset_off_ms'],['reset_on_ms','reset_on_ms'], ['ping_interval_ms','ping_interval_ms'],['restore_mode','restore_mode'], ]; _loadingCfg=true; map.forEach(([id,key])=>{ const el=document.querySelector('[name="'+id+'"]'); if(!el)return; const v=c[key]; if(el.type==='checkbox')el.checked=!!v; else el.value=v!=null?v:''; }); _loadingCfg=false; timezoneOffset=parseTZ(c.timezone); } /* ---- WOL ---- */ var wolDevs=[]; async function loadWol(){ const d=await api('/device/config'); if(!d||!d.config)return; wolDevs=d.config.wol_devices||[]; if(!Array.isArray(wolDevs)){try{wolDevs=JSON.parse(wolDevs)}catch(e){wolDevs=[]}} const el=document.getElementById('wolList'); if(!wolDevs.length){el.innerHTML='
No hay dispositivos registrados
'}else{ el.innerHTML=wolDevs.map((w,i)=>'
'+escapeHtml(w.name)+'
'+escapeHtml(w.mac)+'
').join(''); } const wolBtn=document.getElementById('wolButtons'); if(wolBtn)wolBtn.innerHTML=wolDevs.length?'
'+wolDevs.map(w=>'').join('')+'
':'
No hay dispositivos alternos configurados
'; const sel=document.getElementById('rtnWol'); if(sel)sel.innerHTML=wolDevs.map(w=>'').join(''); } async function wolSend(name){ const d=await api('/command',{method:'POST',body:JSON.stringify({action:'wake_pc',params:{name:name}})}); if(d){t('WOL enviado a: '+name)}else t('Error') } async function addWol(){ const name=document.getElementById('wolName').value.trim(); const mac=document.getElementById('wolMac').value.trim(); if(!name||!mac){t('Completa Nombre y MAC');return} wolDevs.push({name,mac}); const r=await api('/device/config',{method:'POST',body:JSON.stringify({wol_devices:JSON.stringify(wolDevs)})}); if(r){t('Dispositivo agregado');document.getElementById('wolName').value='';document.getElementById('wolMac').value='';loadWol()}else t('Error') } async function delWol(i){ wolDevs.splice(i,1); const r=await api('/device/config',{method:'POST',body:JSON.stringify({wol_devices:JSON.stringify(wolDevs)})}); if(r){t('Dispositivo eliminado');loadWol()}else t('Error') } /* ---- Routines ---- */ async function loadRtns(){ const d=await api('/device/config'); if(!d||!d.config)return; rtns=d.config.routines||[]; if(!Array.isArray(rtns)){try{rtns=JSON.parse(rtns)}catch(e){rtns=[]}} const el=document.getElementById('rtnList'); const acts={ 'power_on':'Encender', 'power_off':'Apagar', 'force_off':'Forzar apagado', 'reset':'Reset', 'wol_primary':'WOL principal', 'wol_device':'WOL alterno' }; if(!rtns.length){el.innerHTML='
No hay rutinas programadas
';return} el.innerHTML=rtns.map((r,i)=>'
'+escapeHtml(acts[r.action]||r.action||'Rutina')+' — '+String(r.hour).padStart(2,'0')+':'+String(r.minute).padStart(2,'0')+' '+(r.enabled?'':'⏸')+'
Repeticiones: '+(r.repeat_count?r.remaining+'/'+r.repeat_count:'Infinitas')+(r.wol_target?' → '+escapeHtml(r.wol_target):'')+'
').join(''); } async function addRtn(){ const h=parseInt(document.getElementById('rtnH').value); const m=parseInt(document.getElementById('rtnM').value); const a=parseInt(document.getElementById('rtnA').value); const wt=document.getElementById('rtnWol').value; const r=parseInt(document.getElementById('rtnR').value); const en=document.getElementById('rtnE').checked; if(isNaN(h)||isNaN(m)){t('Hora y minutos obligatorios');return} rtns.push({hour:h,minute:m,action:a,wol_target:a===5?wt:'',repeat_count:r,remaining:r,enabled:en}); const rr=await api('/device/config',{method:'POST',body:JSON.stringify({routines:JSON.stringify(rtns)})}); if(rr){t('Rutina creada');document.getElementById('rtnH').value='';document.getElementById('rtnM').value='';loadRtns()}else t('Error') } async function delRtn(i){ rtns.splice(i,1); const r=await api('/device/config',{method:'POST',body:JSON.stringify({routines:JSON.stringify(rtns)})}); if(r){t('Rutina eliminada');loadRtns()}else t('Error') } document.getElementById('rtnA').addEventListener('change',function(){document.getElementById('rtnWolF').style.display=this.value==='5'?'block':'none'}); var tutStep=0; var tuts=[ {t:'Bienvenido a Techect',d:'Te guiaremos a traves de las funciones principales del panel de control.'}, {t:'Encendido de PC',d:'Usa el botón de encendido para prender o apagar tu PC de forma remota.'}, {t:'Widgets de información',d:'En la pantalla de inicio verás el estado del dispositivo, la memoria libre y más.'}, {t:'Encendido por LAN',d:'Configura la dirección MAC de tu PC y programa encendidos automáticos.'}, {t:'Rutinas',d:'Crea rutinas para automatizar acciones como encender la PC a una hora específica.'}, {t:'Configuracion',d:'Ajusta la zona horaria, servicios cloud y notificaciones desde Ajustes.'}, {t:'Acceso remoto',d:'Usa Tailscale o WireGuard para acceder al panel desde cualquier lugar.'} ]; function showTutorial(){tutStep=0;document.getElementById('tutO').style.display='flex'} function closeTutorial(){document.getElementById('tutO').style.display='none';localStorage.setItem('tut_done','1')} function updateTut(){ if(tutStep>=tuts.length){closeTutorial();return} document.getElementById('tutTitle').textContent=tuts[tutStep].t; document.getElementById('tutDesc').textContent=tuts[tutStep].d; document.getElementById('tutStep').textContent='Paso '+(tutStep+1)+' de '+tuts.length; document.getElementById('tutPrev').style.display=tutStep>0?'inline-block':'none'; document.getElementById('tutNext').textContent=tutStep0)tutStep--;updateTut()} var _showTutorial=false; /* ---- HID WebSocket ---- */ let ws=null, wsConnected=false, modState=0, tpActive=false, tpLastX=0, tpLastY=0; let wsReconnectTimer=null; let currentFingers=0; let lastClickTime=0, lastClickX=0, lastClickY=0; let tapCount=0, tapTimer=null; let velX=0, velY=0; let animFrame=null; let tpSens=0.5, tpInv=false; function connectWS(){ if(ws&&wsConnected)return; const url='ws://'+location.host+'/ws'; try{ ws=new WebSocket(url); ws.binaryType='arraybuffer'; ws.onopen=()=>{ wsConnected=true; clearTimeout(wsReconnectTimer); ws.send(JSON.stringify({cmd:'status'})); }; ws.onclose=()=>{wsConnected=false;scheduleReconnect();}; ws.onerror=()=>{wsConnected=false;}; ws.onmessage=e=>{ if(typeof e.data==='string'){ try{ const d=JSON.parse(e.data); if(d.type==='state' && d.hid_connected!==undefined){ document.getElementById('btDot').className='sdot '+(d.hid_connected?'on':'off'); document.getElementById('btStat').textContent=d.hid_connected?'Conectado':'Sin conexión'; } }catch(_){} } }; }catch(e){scheduleReconnect();} } function scheduleReconnect(){ clearTimeout(wsReconnectTimer); wsReconnectTimer=setTimeout(connectWS,3000); } function wsSend(data){ if(!ws||!wsConnected)return; try{ws.send(data)}catch(_){} } function hidMouseDown(btn){ const buf=new ArrayBuffer(2);const d=new Uint8Array(buf); d[0]=3;d[1]=btn;wsSend(buf); } function hidMouseUp(){ const buf=new ArrayBuffer(2);const d=new Uint8Array(buf); d[0]=3;d[1]=0;wsSend(buf); } function hidClick(btn){ hidMouseDown(btn); setTimeout(hidMouseUp,50); } function hidScroll(val){ const buf=new ArrayBuffer(2);const d=new Uint8Array(buf); d[0]=2;d[1]=val;wsSend(buf); } function hidKey(key,press,mod=modState){ wsSend(JSON.stringify({cmd:'hid',action:'kb',mod,key:parseInt(key),press})); } function toggleMod(el){ el.classList.toggle('on'); let m=parseInt(el.dataset.mod); if(el.classList.contains('on'))modState|=m;else modState&=~m; } function toggleKey(el){ el.classList.toggle('on'); if(el.classList.contains('on')){ const k=el.dataset.k; hidKey(k,true,modState); }else{ const k=el.dataset.k; hidKey(k,false,modState); } } function inputKeydown(e){ if(e.key==='Backspace'){ e.preventDefault(); hidKey('0x2A',true); setTimeout(()=>hidKey('0x2A',false),10); } else if(e.key==='Enter'){ e.preventDefault(); hidKey('0x28',true); setTimeout(()=>hidKey('0x28',false),10); } } const BASE_MAPPING = { 'a':0x04,'b':0x05,'c':0x06,'d':0x07,'e':0x08,'f':0x09,'g':0x0A,'h':0x0B,'i':0x0C,'j':0x0D,'k':0x0E,'l':0x0F,'m':0x10,'n':0x11,'o':0x12,'p':0x13,'q':0x14,'r':0x15,'s':0x16,'t':0x17,'u':0x18,'v':0x19,'w':0x1A,'x':0x1B,'y':0x1C,'z':0x1D, '1':0x1E,'2':0x1F,'3':0x20,'4':0x21,'5':0x22,'6':0x23,'7':0x24,'8':0x25,'9':0x26,'0':0x27, 'Enter':0x28,'Escape':0x29,'Backspace':0x2A,'Tab':0x2B,'Space':0x2C,'-':0x2D,'=':0x2E,'[':0x2F,']':0x30,'\\':0x31,';':0x33,'\'':0x34,'`':0x35,',':0x36,'.':0x37,'/':0x38,'CapsLock':0x39, 'F1':0x3A,'F2':0x3B,'F3':0x3C,'F4':0x3D,'F5':0x3E,'F6':0x3F,'F7':0x40,'F8':0x41,'F9':0x42,'F10':0x43,'F11':0x44,'F12':0x45, 'Insert':0x49,'Home':0x4A,'PageUp':0x4B,'Delete':0x4C,'End':0x4D,'PageDown':0x4E,'Right':0x4F,'Left':0x50,'Down':0x51,'Up':0x52, '!':{key:0x1E,mod:2},'@':{key:0x1F,mod:2},'#':{key:0x20,mod:2},'$':{key:0x21,mod:2},'%':{key:0x22,mod:2},'^':{key:0x23,mod:2},'&':{key:0x24,mod:2},'*':{key:0x25,mod:2},'(': {key:0x26,mod:2},')':{key:0x27,mod:2}, '_':{key:0x2D,mod:2},'+':{key:0x2E,mod:2},'{':{key:0x2F,mod:2},'}':{key:0x30,mod:2},'|':{key:0x31,mod:2},':':{key:0x33,mod:2},'"':{key:0x34,mod:2},'~':{key:0x35,mod:2},'<':{key:0x36,mod:2},'>':{key:0x37,mod:2},'?':{key:0x38,mod:2}, 'A':{key:0x04,mod:2},'B':{key:0x05,mod:2},'C':{key:0x06,mod:2},'D':{key:0x07,mod:2},'E':{key:0x08,mod:2},'F':{key:0x09,mod:2},'G':{key:0x0A,mod:2},'H':{key:0x0B,mod:2},'I':{key:0x0C,mod:2},'J':{key:0x0D,mod:2},'K':{key:0x0E,mod:2},'L':{key:0x0F,mod:2},'M':{key:0x10,mod:2},'N':{key:0x11,mod:2},'O':{key:0x12,mod:2},'P':{key:0x13,mod:2},'Q':{key:0x14,mod:2},'R':{key:0x15,mod:2},'S':{key:0x16,mod:2},'T':{key:0x17,mod:2},'U':{key:0x18,mod:2},'V':{key:0x19,mod:2},'W':{key:0x1A,mod:2},'X':{key:0x1B,mod:2},'Y':{key:0x1C,mod:2},'Z':{key:0x1D,mod:2} }; let currentMapping = { ...BASE_MAPPING }; function applyLocaleMapping() { const lang = navigator.language || 'en-US'; if (lang.startsWith('es')) { currentMapping['ñ'] = 0x33; currentMapping['Ñ'] = { key: 0x33, mod: 2 }; currentMapping['á'] = 0x34; currentMapping['Á'] = { key: 0x34, mod: 2 }; currentMapping['é'] = 0x35; currentMapping['É'] = { key: 0x35, mod: 2 }; currentMapping['í'] = 0x23; currentMapping['Í'] = { key: 0x23, mod: 2 }; currentMapping['ó'] = 0x24; currentMapping['Ó'] = { key: 0x24, mod: 2 }; currentMapping['ú'] = 0x25; currentMapping['Ú'] = { key: 0x25, mod: 2 }; if (lang === 'es-ES' || lang === 'es') { currentMapping['?'] = 0x30; currentMapping['_'] = { key: 0x30, mod: 2 }; } } } function sendKey(ch){ const mapping = currentMapping[ch]; if(!mapping) return; const key = typeof mapping === 'object' ? mapping.key : mapping; const mod = typeof mapping === 'object' ? mapping.mod : 0; hidKey('0x'+key.toString(16).toUpperCase().padStart(2,'0'),true,mod); setTimeout(()=>hidKey('0x'+key.toString(16).toUpperCase().padStart(2,'0'),false,mod),5); } function inputHandler(){ const el=document.getElementById('kbText'); const t=el.value; if(t.length===0)return; const ch=t[t.length-1]; el.value=''; sendKey(ch); } function hidConsumer(key){ const buf=new ArrayBuffer(4);const d=new Uint8Array(buf); d[0]=6;d[1]=key&0xFF;d[2]=(key>>8)&0xFF;d[3]=1;wsSend(buf); setTimeout(()=>{d[3]=0;wsSend(buf.slice(0))},80); } function hidGamepad(x,y,z,rz,buttons,hat){ const buf=new ArrayBuffer(7);const d=new Uint8Array(buf); d[0]=5;d[1]=x;d[2]=y;d[3]=z;d[4]=rz;d[5]=buttons;d[6]=hat;wsSend(buf); } const tpCanvas=document.getElementById('tpCanvas'); const tpCanvasFull=document.getElementById('tpCanvasFull'); const tpCtx=tpCanvas.getContext('2d'); const tpCtxFull=tpCanvasFull.getContext('2d'); let tpDrawing=false, tpActiveCanvas=tpCanvas; function updateSens(){ tpSens = parseFloat(document.getElementById('tpSens').value); document.getElementById('sensVal').textContent = tpSens.toFixed(2); localStorage.setItem('tpSens', tpSens); } function updateInv(){ tpInv = document.getElementById('tpInv').checked; localStorage.setItem('tpInv', tpInv); } function loadTrackpadSettings(){ const savedSens = localStorage.getItem('tpSens'); const savedInv = localStorage.getItem('tpInv'); if(savedSens !== null){ tpSens = parseFloat(savedSens); document.getElementById('tpSens').value = tpSens; document.getElementById('sensVal').textContent = tpSens.toFixed(2); } if(savedInv !== null){ tpInv = savedInv === 'true'; document.getElementById('tpInv').checked = tpInv; } } function resizeTP(){const r=tpCanvas.parentElement.getBoundingClientRect();tpCanvas.width=r.width*tpCanvas.offsetHeight/r.height;tpCanvas.height=tpCanvas.offsetHeight;drawTPGrid(tpCanvas,tpCtx)} function resizeTPFull(){const r=tpCanvasFull.parentElement.getBoundingClientRect();tpCanvasFull.width=r.width;tpCanvasFull.height=r.height;drawTPGrid(tpCanvasFull,tpCtxFull)} function drawTPGrid(canvas,ctx){ctx.fillStyle='#1e293b';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.strokeStyle='#2e3d56';ctx.lineWidth=1;for(let x=0;x= 3) hidMouseDown(4); } function tpMove(e){ if(!tpDrawing) return; const touches = e.touches; if(touches && touches.length === 2){ e.preventDefault(); const centerY = (touches[0].clientY + touches[1].clientY) / 2; const dy = centerY - tpLastY; tpLastY = centerY; if(Math.abs(dy) > 1){ const scrollVal = tpInv ? dy * 0.2 : -dy * 0.2; hidScroll(Math.round(scrollVal)); } return; } const p=e.touches?{x:e.touches[0].clientX,y:e.touches[0].clientY}:{x:e.clientX,y:e.clientY}; e.preventDefault(); const dx=p.x-tpLastX, dy=p.y-tpLastY; tpLastX=p.x; tpLastY=p.y; const speed = Math.hypot(dx, dy); const accel = speed > 50 ? 2.5 : (speed > 20 ? 1.8 : 1.2); const maxVel = 120; velX = Math.max(-maxVel, Math.min(maxVel, velX * 0.85 + dx * accel * tpSens)); velY = Math.max(-maxVel, Math.min(maxVel, velY * 0.85 + dy * accel * tpSens)); if(Math.abs(velX) > 0.5 || Math.abs(velY) > 0.5){ sendMouseMove(velX, velY); } } function sendMouseMove(dx, dy){ const buf=new ArrayBuffer(3); const d=new Uint8Array(buf); d[0]=1; d[1]=Math.round(dx)&0xFF; d[2]=Math.round(dy)&0xFF; wsSend(buf); } function tpEnd(e){ if(!tpDrawing)return; tpDrawing=false; if(tpActiveCanvas===tpCanvas) tpCanvas.parentElement.classList.remove('active'); function decelerate(){ velX *= 0.92; velY *= 0.92; if(Math.abs(velX) > 0.5 || Math.abs(velY) > 0.5){ sendMouseMove(velX, velY); animFrame = requestAnimationFrame(decelerate); } } decelerate(); const p=e.changedTouches?{x:e.changedTouches[0].clientX,y:e.changedTouches[0].clientY}:{x:e.clientX,y:e.clientY}; const dist=Math.hypot(p.x-tpStartX,p.y-tpStartY); if(dist<10){ const now = Date.now(); if(now - lastClickTime < 300 && Math.hypot(p.x-lastClickX, p.y-lastClickY) < 10){ // Double click: first click already completed (down in tpStart, up here), send second click hidClick(currentFingers === 1 ? 1 : currentFingers === 2 ? 2 : 4); } else { // Single click: tpStart sent down, just send up to complete } lastClickTime = now; lastClickX = p.x; lastClickY = p.y; } hidMouseUp(); } function toggleFullscreenTP(){ const overlay=document.getElementById('tpOverlay'); overlay.classList.toggle('show'); tpActiveCanvas=overlay.classList.contains('show')?tpCanvasFull:tpCanvas; if(tpActiveCanvas===tpCanvasFull) resizeTPFull(); } function exitFullscreenTP(){ cancelAnimationFrame(animFrame); document.getElementById('tpOverlay').classList.remove('show'); tpActiveCanvas=tpCanvas; } function toggleFullscreenGP(){ const overlay=document.getElementById('gpOverlay'); overlay.classList.toggle('show'); if(overlay.classList.contains('show')){ const content = document.getElementById('gpadContent').cloneNode(true); content.style.display = 'flex'; content.style.gap = '60px'; content.style.alignItems = 'center'; content.style.justifyContent = 'center'; content.className = 'gp-immersive'; document.getElementById('gpOverlayBody').innerHTML = ''; document.getElementById('gpOverlayBody').appendChild(content); } } function exitFullscreenGP(){ document.getElementById('gpOverlay').classList.remove('show'); } tpCanvas.addEventListener('mousedown',tpStart); tpCanvasFull.addEventListener('mousedown',tpStart); document.addEventListener('mousemove',tpMove); document.addEventListener('mouseup',tpEnd); tpCanvas.addEventListener('touchstart',tpStart,{passive:false}); tpCanvasFull.addEventListener('touchstart',tpStart,{passive:false}); document.addEventListener('touchmove',tpMove,{passive:false}); document.addEventListener('touchend',tpEnd,{passive:false}); document.querySelectorAll('.kb-k[data-k]').forEach(btn=>{ btn.addEventListener('mousedown',e=>{e.preventDefault();hidKey(btn.dataset.k,true)}); btn.addEventListener('mouseup',e=>{hidKey(btn.dataset.k,false)}); btn.addEventListener('mouseleave',e=>{hidKey(btn.dataset.k,false)}); btn.addEventListener('touchstart',e=>{e.preventDefault();hidKey(btn.dataset.k,true)},{passive:false}); btn.addEventListener('touchend',e=>{hidKey(btn.dataset.k,false)}); btn.addEventListener('touchcancel',e=>{hidKey(btn.dataset.k,false)}); }); async function refresh(){lastRefresh=Date.now();await loadDev()} function parseTZ(tz){ if(!tz)return 0; const m=tz.match(/UTC([+-])(\d+)(?::(\d+))?/); if(!m)return 0; let o=parseInt(m[2])*60; if(m[3])o+=parseInt(m[3]); return m[1]==='-'?-o:o; } function updateClock(){ const now = new Date(); const utc = now.getTime(); const esp = new Date(utc + timezoneOffset * 60000); document.getElementById('clock').textContent = esp.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit', timeZone: 'UTC', hour12: false }); } setInterval(updateClock,1000); updateClock(); var tutStep=0; var tutSteps=[ {title:'Bienvenido al panel',text:'Este es el panel de control de Techect. Desde acá puedes encender y apagar tu PC, configurar rutinas programadas, administrar dispositivos WOL y mucho más.'}, {title:'Encender tu PC',text:'En la pestaña Inicio ves el estado actual de tu PC. Presiona el botón central para encenderla o apagarla. También puedes reiniciarla o forzar un apagado.'}, {title:'Control remoto',text:'En la pestaña Remoto tienes un trackpad, teclado virtual, controles multimedia y gamepad. El WebSocket se conecta automáticamente al cargar la página.'}, {title:'Rutinas y ajustes',text:'Programa encendidos y apagados automáticos en Rutinas. Configura notificaciones, zona horaria y servicios cloud en Ajustes.'} ]; function showTutorial(){var e=document.getElementById('tutorial');e.style.display='flex';e.classList.remove('hid')} function hideTutorial(){var e=document.getElementById('tutorial');e.classList.add('hid');e.style.display='none';localStorage.setItem('tut_done','1')} function updateTut(){ var s=tutSteps[tutStep]; document.getElementById('tutContent').innerHTML='

'+escapeHtml(s.title)+'

'+s.text+'

'; document.querySelectorAll('.step-dot').forEach(function(d,i){ d.className='step-dot'+(i===tutStep?' active':'')+(i0){tutStep--;updateTut()}} function boot(){ applyLocaleMapping(); applyTheme(); loadTrackpadSettings(); refresh(); setTimeout(loadCfg,100); setTimeout(loadWol,200); setTimeout(loadRtns,300); setTimeout(resizeTP,500); setTimeout(connectWS,600); window.addEventListener('resize',resizeTP); document.addEventListener('visibilitychange',()=>{ if(!document.hidden&&Date.now()-lastRefresh>30000){ refresh(); } }); setInterval(refresh, 30000); } if(localStorage.getItem('sec')&&localStorage.getItem('d')){start()}else{boot()}