feat(ao-vivo): avisa bateria baixa e mostra o ultimo sinal do celular

- aviso no painel quando a bateria do celular chega a 15% e a 5% sem
  carregador (uma vez cada; volta a valer depois de carregar)
- selo do celular fica vermelho com bateria baixa
- celular offline mostra ha quanto tempo foi o ultimo sinal e com quanta
  bateria; abaixo de 10% sugere que a bateria acabou

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-06 02:24:33 -03:00
co-authored by Claude Opus 5.5
parent 69018c8b36
commit f45e05ab81
4 changed files with 70 additions and 14 deletions
+33 -1
View File
@@ -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<string, (d: any) => void> = {
},
telemetria: (d: Telemetria) => {
estado.app = d;
estado.appRecebidoEm = Date.now();
estado.appRecebidoEm = estado.ultimoSinalEm = Date.now();
avisarBateria();
},
app_config: (d: AppConfig) => (estado.appConfig = d),
aparencia: (d) => (estado.aparencia = d),