+
Abra o app Carinha no celular e confira se ele aponta para este servidor (toque longo › Mudar o servidor).
{/if} @@ -170,6 +180,17 @@ rect.cheia { fill: var(--texto); } + .ultimo { + display: flex; + align-items: center; + gap: var(--e-2); + flex-wrap: wrap; + font-size: var(--t-sm); + font-weight: 500; + } + .ultimo.baixa { + color: var(--perigo); + } .ajuda { font-size: var(--t-sm); color: var(--texto-3); diff --git a/web/src/lib/estado.svelte.ts b/web/src/lib/estado.svelte.ts index 8903046..e1ab0cf 100644 --- a/web/src/lib/estado.svelte.ts +++ b/web/src/lib/estado.svelte.ts @@ -3,6 +3,7 @@ * Nenhuma tela faz polling; todas leem daqui. */ import { ErroApi, pedir, quandoNegado, urlEventos } from './api'; +import { avisos } from './avisos.svelte'; import type { AppConfig, Conversa, EstadoServidor, IA, Motores, Reacao, Telemetria } from './tipos'; export type Conexao = 'conectando' | 'aberta' | 'caiu' | 'token'; @@ -37,6 +38,18 @@ class Estado { agora = $state(Date.now()); celularOnline = $derived(!!this.app?.online && this.agora - this.appRecebidoEm < 6000); + /** Quando o celular mandou o último sinal (0 = nunca, nesta sessão nem no servidor). */ + ultimoSinalEm = $state(0); + /** Bateria do último sinal recebido, mesmo com o celular offline. */ + bateria = $derived.by(() => { + const b = this.app?.estado?.bateria; + const n = typeof b === 'number' ? b : b?.nivel; + return { + nivel: typeof n === 'number' ? Math.round(n <= 1 ? n * 100 : n) : null, + carregando: typeof b === 'object' && !!b?.carregando, + }; + }); + bateriaBaixa = $derived(this.bateria.nivel !== null && this.bateria.nivel <= 15 && !this.bateria.carregando); } export const estado = new Estado(); @@ -51,6 +64,23 @@ export function ouvir(tipo: string, fn: Ouvinte): () => void { return () => ouvintes.get(tipo)!.delete(fn); } +// Avisa uma vez em 15% e outra em 5% (sem carregador); zera quando volta a carregar. +let avisouEm: number | null = null; +function avisarBateria() { + const { nivel, carregando } = estado.bateria; + if (nivel === null) return; + if (carregando || nivel > 20) { + avisouEm = null; + return; + } + const limite = nivel <= 5 ? 5 : nivel <= 15 ? 15 : null; + if (limite !== null && avisouEm !== limite) { + avisouEm = limite; + avisos.mostrar(`Bateria do celular em ${nivel}%. Ligue o carregador ou o Carinha vai desligar.`, 'erro'); + noFeed('estop', `Bateria do celular em ${nivel}%`); + } +} + let seqFeed = 0; function noFeed(tipo: string, texto: string) { estado.feed = [{ id: ++seqFeed, quando: Date.now(), tipo, texto }, ...estado.feed].slice(0, 40); @@ -67,6 +97,7 @@ function aplicarFoto(f: EstadoServidor) { if (f.app) { estado.app = f.app; estado.appRecebidoEm = Date.now(); + estado.ultimoSinalEm = typeof f.app.idade === 'number' ? Date.now() - f.app.idade * 1000 : 0; } if (f.app_config) estado.appConfig = f.app_config; if (f.motores) estado.motores = f.motores; @@ -83,7 +114,8 @@ const tratadores: Record