feat(web): cria painel Svelte com shell, Ao vivo e Ajustes
- web/ com Vite + Svelte 5 + TypeScript; servidor entrega web/dist em "/" (telas antigas seguem em /dashboard, /painel, /braco...) - design system em tokens CSS, tema escuro e claro, fontes locais - estado unico: foto de /estado + um EventSource em /eventos, sem polling - shell com navegacao lateral/inferior, selos de status reais e E-Stop fixo - command palette (Ctrl+K), gestos favoritos com atalhos 1-9, g+letra navega - Ao vivo: camera WebRTC e rastreamento portados para TS, rosto, aparencia, sensores do celular, IA local e feed de acontecimentos - Ajustes: campos do celular agrupados com busca, salvamento otimista e restaurar padrao (GET /app/padrao); motor da conversa, Gemini e token - carinha.html ganha modo ?painel=1 dirigido por postMessage (sem polling) - teste Playwright de ponta a ponta do painel novo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/** Cliente HTTP único do painel: token, JSON, tempo limite e erro legível. */
|
||||
|
||||
const CHAVE_TOKEN = 'carinha_token';
|
||||
|
||||
export class ErroApi extends Error {
|
||||
constructor(public status: number, mensagem: string) {
|
||||
super(mensagem);
|
||||
}
|
||||
}
|
||||
|
||||
export function lerToken(): string {
|
||||
try {
|
||||
return localStorage.getItem(CHAVE_TOKEN) || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function gravarToken(token: string) {
|
||||
try {
|
||||
if (token) localStorage.setItem(CHAVE_TOKEN, token);
|
||||
else localStorage.removeItem(CHAVE_TOKEN);
|
||||
} catch {
|
||||
/* navegação privada: vale só nesta aba */
|
||||
}
|
||||
}
|
||||
|
||||
/** Avisado quando o servidor recusa o token (abre o diálogo de token). */
|
||||
let aoNegar: () => void = () => {};
|
||||
export function quandoNegado(fn: () => void) {
|
||||
aoNegar = fn;
|
||||
}
|
||||
|
||||
export async function pedir<T = any>(caminho: string, dados?: unknown, tempo = 8000): Promise<T> {
|
||||
const cabecalhos: Record<string, string> = {};
|
||||
const token = lerToken();
|
||||
if (token) cabecalhos.Authorization = 'Bearer ' + token;
|
||||
const opcoes: RequestInit = { cache: 'no-store', headers: cabecalhos, signal: AbortSignal.timeout(tempo) };
|
||||
if (dados !== undefined) {
|
||||
opcoes.method = 'POST';
|
||||
cabecalhos['Content-Type'] = 'application/json';
|
||||
opcoes.body = JSON.stringify(dados);
|
||||
}
|
||||
let resposta: Response;
|
||||
try {
|
||||
resposta = await fetch(caminho, opcoes);
|
||||
} catch (e) {
|
||||
const nome = (e as Error).name;
|
||||
throw new ErroApi(0, nome === 'TimeoutError' ? 'O servidor demorou a responder' : 'Sem conexão com o servidor');
|
||||
}
|
||||
const texto = await resposta.text();
|
||||
let corpo: any = null;
|
||||
try {
|
||||
corpo = texto ? JSON.parse(texto) : null;
|
||||
} catch {
|
||||
corpo = null;
|
||||
}
|
||||
if (!resposta.ok) {
|
||||
if (resposta.status === 401) aoNegar();
|
||||
throw new ErroApi(resposta.status, corpo?.erro || `Erro ${resposta.status}`);
|
||||
}
|
||||
return corpo as T;
|
||||
}
|
||||
|
||||
export const postar = <T = any>(caminho: string, dados: unknown = {}) => pedir<T>(caminho, dados);
|
||||
|
||||
export function urlEventos(): string {
|
||||
const token = lerToken();
|
||||
return token ? '/eventos?t=' + encodeURIComponent(token) : '/eventos';
|
||||
}
|
||||
Reference in New Issue
Block a user