- 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>
71 lines
2.1 KiB
TypeScript
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';
|
|
}
|