feat(celular): rosto e app usam SSE e token; remove telas legadas
- carinha.html assina /eventos (expressao, app_config, conversa, reacao, recarregar) e volta ao polling de /fila sozinho se o SSE cair; foto dos ajustes a cada (re)conexao; token (?t=) vai em todo fetch ao servidor - APK 0.8-token: token nas preferencias e no menu de toque longo, enviado na pagina, no HTTP do Java e ao llama-server (--api-key); trocar servidor por Intent exige o token salvo; camera/microfone so para o servidor configurado ou a copia local - cerebro/conversa mandam a chave ao llama-server do S10 - servidor: evento SSE 'recarregar'; remove /camera/frame e /app/connect; enderecos antigos redirecionam para a secao do painel novo - remove dashboard, painel, conversa, emojis, treinamento, braco, servos, api, scripts e CSS antigos, audio-robot.js e servir-local.bat - logica pura do painel em web/src/lib/nucleo.ts com testes node --test (substitui test_movimentos.js e test_rastreamento.js) - testes de navegador portados para o painel novo (rtc, rosto, rastreamento) - README, docs/PAINEL.md e docs/SEGURANCA.md reescritos; versao 0.8.0; carinha-pc.service le /etc/carinha.env Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,25 @@
|
||||
</svg>
|
||||
<div id="fps"></div>
|
||||
<script>
|
||||
/* Token do servidor (o app Android abre a página com ?t=...): vai em todo pedido ao
|
||||
próprio servidor, inclusive os de reacoes-render.js. Sem ?t= nada muda. */
|
||||
(function () {
|
||||
const token = new URLSearchParams(location.search).get('t');
|
||||
window.carinhaToken = token || '';
|
||||
if (!token || !window.fetch) return;
|
||||
const original = window.fetch.bind(window);
|
||||
window.fetch = (recurso, opcoes = {}) => {
|
||||
try {
|
||||
const url = new URL(typeof recurso === 'string' ? recurso : recurso.url, location.href);
|
||||
if (url.origin === location.origin) {
|
||||
const cabecalhos = new Headers(opcoes.headers || {});
|
||||
cabecalhos.set('Authorization', 'Bearer ' + token);
|
||||
opcoes = Object.assign({}, opcoes, { headers: cabecalhos });
|
||||
}
|
||||
} catch (e) { /* URL estranha: segue sem token */ }
|
||||
return original(recurso, opcoes);
|
||||
};
|
||||
})();
|
||||
"use strict";
|
||||
/* -------------------------------------------------------------------------
|
||||
Carinha estilo Eilik em SVG — porte fiel do eilik.py (Python/framebuffer).
|
||||
@@ -424,6 +443,9 @@ function trata(ordem) {
|
||||
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
||||
} else if (SEQS[ordem]) {
|
||||
animar(ordem);
|
||||
} else if (ordem === "calma") {
|
||||
// E-Stop do painel: interrompe a animação e volta ao rosto neutro.
|
||||
r.fila = []; r.kf = null; r.boca = "neutra"; r.dormindo = false;
|
||||
} else if (ordem === "parar") {
|
||||
try { window.close(); } catch (e) { r.fila = []; }
|
||||
}
|
||||
@@ -460,7 +482,7 @@ window.eilik = {
|
||||
// busca ordens no servidorzinho da placa (/fila) — assim da para mandar gesto
|
||||
// do PC com um curl, igual ao FIFO de antes
|
||||
async function buscarOrdens() {
|
||||
if (new URLSearchParams(location.search).get('espelho') === '1') return;
|
||||
if (new URLSearchParams(location.search).get('espelho') === '1' || window.carinhaSSE) return;
|
||||
try {
|
||||
const resp = await fetch("fila"+(par.get("aparelho")==="1"?"?aparelho=1":""), { cache: "no-store" });
|
||||
if (!resp.ok) return;
|
||||
@@ -484,7 +506,18 @@ async function aplicarAparencia() {
|
||||
} catch (e) { /* sem servidor — segue com o padrao */ }
|
||||
}
|
||||
setTimeout(aplicarAparencia, 200);
|
||||
if (new URLSearchParams(location.search).get('espelho') === '1') setInterval(aplicarAparencia, 1000);
|
||||
// ?painel=1: prévia dentro do painel novo. Nada de polling; o painel repassa os eventos
|
||||
// do servidor (SSE) por postMessage.
|
||||
const MODO_PAINEL = new URLSearchParams(location.search).get('painel') === '1';
|
||||
if (MODO_PAINEL) {
|
||||
window.carinhaAtiva = false;
|
||||
addEventListener('message', (e) => {
|
||||
if (e.origin !== location.origin || e.source !== parent || !e.data) return;
|
||||
const m = e.data;
|
||||
if (m.tipo === 'ordem' && typeof m.ordem === 'string') trata(m.ordem);
|
||||
else if (m.tipo === 'config' && m.config) configurarApp(m.config);
|
||||
});
|
||||
} else if (new URLSearchParams(location.search).get('espelho') === '1') setInterval(aplicarAparencia, 1000);
|
||||
|
||||
// teclado (util para testar no PC)
|
||||
const TECLAS = { p: "piscar", f: "feliz", c: "coracao", s: "surpreso", b: "bravo",
|
||||
@@ -818,8 +851,10 @@ async function sincronizarApp() {
|
||||
if (sincronizando || location.protocol === 'file:' || window.carinhaAtiva === false) return;
|
||||
sincronizando = true;
|
||||
try {
|
||||
const resp = await fetch('/app/config', { cache: 'no-store', signal: AbortSignal.timeout(2500) });
|
||||
if (resp.ok) configurarApp(await resp.json());
|
||||
if (!window.carinhaSSE) {
|
||||
const resp = await fetch('/app/config', { cache: 'no-store', signal: AbortSignal.timeout(2500) });
|
||||
if (resp.ok) configurarApp(await resp.json());
|
||||
}
|
||||
if (par.get('aparelho') === '1') {
|
||||
const agora = performance.now() / 1000;
|
||||
await fetch('/app/status', { method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
@@ -965,7 +1000,7 @@ setTimeout(relatar, 1500);
|
||||
if(location.protocol==='file:')return;
|
||||
let seen=null,fetching=false;
|
||||
async function poll(){
|
||||
if(fetching||window.carinhaAtiva===false)return;fetching=true;
|
||||
if(fetching||window.carinhaAtiva===false||window.carinhaSSE)return;fetching=true;
|
||||
try{const response=await fetch('/conversa/status',{cache:'no-store',signal:AbortSignal.timeout(2500)});if(!response.ok)return;
|
||||
const s=await response.json(),m=s.ultima;if(!m){seen=null;return;}
|
||||
if(m.id===seen||Date.now()/1000-m.horario>60)return;seen=m.id;
|
||||
@@ -977,11 +1012,50 @@ setTimeout(relatar, 1500);
|
||||
|
||||
</script>
|
||||
<script src="/reacoes-render.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script>
|
||||
if(new URLSearchParams(location.search).get('aparelho')==='1' && new URLSearchParams(location.search).has('atualizacao')) {
|
||||
const aviso=document.createElement('div');aviso.id='avisoRecarga';aviso.textContent='Tela atualizada';aviso.style.cssText='position:fixed;bottom:8%;left:0;right:0;text-align:center;color:#fff;font:24px sans-serif;z-index:100;pointer-events:none';document.body.append(aviso);setTimeout(()=>aviso.remove(),4000);
|
||||
}
|
||||
|
||||
/* Tempo real: um EventSource em /eventos substitui os pollings de /fila, /app/config,
|
||||
/conversa/status e /treinamento/expressao. Se cair, os pollings voltam sozinhos
|
||||
(window.carinhaSSE=false) até o EventSource reconectar. */
|
||||
(function () {
|
||||
const p = new URLSearchParams(location.search);
|
||||
if (location.protocol === 'file:' || p.get('espelho') === '1' || !window.EventSource) return;
|
||||
const aparelho = p.get('aparelho') === '1';
|
||||
const t = window.carinhaToken;
|
||||
const fonte = new EventSource('/eventos' + (t ? '?t=' + encodeURIComponent(t) : ''));
|
||||
let ultimaConversa = null, ultimaReacao = null, primeira = true;
|
||||
const ouvir = (tipo, fn) => fonte.addEventListener(tipo, (e) => { try { fn(JSON.parse(e.data)); } catch (err) {} });
|
||||
fonte.addEventListener('pronto', () => {
|
||||
// Ordens que chegaram antes da conexão ficam na fila: executa uma vez e passa ao SSE.
|
||||
if (primeira) { primeira = false; buscarOrdens(); }
|
||||
window.carinhaSSE = true;
|
||||
// Foto dos ajustes a cada (re)conexão; depois só mudanças chegam por 'app_config'.
|
||||
fetch('/app/config', { cache: 'no-store' }).then((r) => r.ok && r.json()).then((c) => c && configurarApp(c)).catch(() => {});
|
||||
});
|
||||
fonte.onerror = () => {
|
||||
if (!window.carinhaSSE) return;
|
||||
window.carinhaSSE = false;
|
||||
// Volta ao polling sem repetir o que o SSE já entregou.
|
||||
fetch('fila' + (aparelho ? '?aparelho=1' : ''), { cache: 'no-store' }).catch(() => {});
|
||||
};
|
||||
ouvir('expressao', (d) => trata(d.ordem));
|
||||
ouvir('recarregar', () => { if (aparelho) trata('recarregar'); });
|
||||
ouvir('app_config', (c) => configurarApp(c));
|
||||
ouvir('conversa', (s) => {
|
||||
const m = s.ultima;
|
||||
if (!m || m.id === ultimaConversa || Date.now() / 1000 - m.horario > 60) return;
|
||||
ultimaConversa = m.id;
|
||||
MIC.ultimoSom = performance.now() / 1000; trata('acordar'); trata(m.gesto);
|
||||
});
|
||||
ouvir('reacao', (d) => {
|
||||
if (!d || d.id === ultimaReacao || Date.now() / 1000 - d.horario > 60) return;
|
||||
ultimaReacao = d.id;
|
||||
if (window.eilik && window.eilik.receita) window.eilik.receita(d.receita);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user