Files
carinha-control-panel/web/src/lib/api.ts
T
julioandClaude Opus 5.5 c4b53a8976 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>
2026-10-05 19:47:18 -03:00

71 lines
2.1 KiB
TypeScript

/** 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';
}