feat(eventos): menu Eventos, modo presenca e 3 dias de historico
- "Eventos" vira item do menu principal: lista por dia (Hoje, Ontem...), presenca e movimento em destaque, botao "Ver foto" com miniatura - modo presenca: o PIR liga a camera do celular e acorda o Carinha; sem ninguem por N minutos (5 por padrao) a camera desliga, ele dorme e fica em silencio (sons e fala desligados enquanto dorme) - historico guarda so os ultimos 3 dias (ajustavel de 1 a 30), com faxina automatica de eventos e fotos antigos - testes da retencao e do modo presenca Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ class Estado {
|
||||
api = $state<EstadoServidor['api'] | null>(null);
|
||||
feed = $state<ItemFeed[]>([]);
|
||||
/** Sensor PIR da placa: último disparo (s, epoch) e se está ligado. */
|
||||
pir = $state<{ disponivel: boolean; ultimo: number | null } | null>(null);
|
||||
pir = $state<{ disponivel: boolean; ultimo: number | null; presenca?: { estado: 'presente' | 'ausente'; desde: number; ultimo: number } } | null>(null);
|
||||
/** Relógio de 1 s para derivar "online" sem esperar evento novo. */
|
||||
agora = $state(Date.now());
|
||||
|
||||
@@ -108,7 +108,7 @@ function aplicarFoto(f: EstadoServidor) {
|
||||
estado.reacao = f.reacao ?? null;
|
||||
if (f.treinamento) estado.treinamento = f.treinamento;
|
||||
if (f.api) estado.api = f.api;
|
||||
if (f.pir) estado.pir = { disponivel: f.pir.disponivel, ultimo: f.pir.ultimo };
|
||||
if (f.pir) estado.pir = { disponivel: f.pir.disponivel, ultimo: f.pir.ultimo, presenca: (f.pir as any).presenca };
|
||||
}
|
||||
|
||||
const tratadores: Record<string, (d: any) => void> = {
|
||||
@@ -142,7 +142,7 @@ const tratadores: Record<string, (d: any) => void> = {
|
||||
if (d?.receita?.nome) noFeed('reacao', 'Reação: ' + d.receita.nome);
|
||||
},
|
||||
treinamento: (d) => (estado.treinamento = d),
|
||||
pir: (d: { ultimo: number }) => (estado.pir = { disponivel: true, ultimo: d.ultimo }),
|
||||
pir: (d: { ultimo: number; presenca?: any }) => (estado.pir = { disponivel: true, ultimo: d.ultimo, presenca: d.presenca }),
|
||||
historico: (d) => {
|
||||
const n = d?.novo;
|
||||
if (n) noFeed('historico', { pir: '🚶 Presença no PIR', movimento: '👀 Movimento na câmera', som: '🔊 Som alto' }[n.tipo as string] ?? 'Detecção');
|
||||
|
||||
@@ -13,7 +13,8 @@ export interface EventoHistorico {
|
||||
}
|
||||
|
||||
export interface StatusHistorico {
|
||||
config: { pir: boolean; movimento: boolean; som: boolean; foto: boolean; intervalo: number };
|
||||
config: { pir: boolean; movimento: boolean; som: boolean; foto: boolean; intervalo: number; dias: number; presenca: boolean; ausencia: number };
|
||||
presenca: { estado: 'presente' | 'ausente'; desde: number; ultimo: number };
|
||||
pir: { disponivel: boolean; erro: string; linha: string; ultimo: number | null };
|
||||
eventos: EventoHistorico[];
|
||||
total: number;
|
||||
|
||||
@@ -36,7 +36,6 @@ export function itensDaPaleta(): ItemPaleta[] {
|
||||
registrarNaPaleta(() => [
|
||||
...ROTAS.map((r) => ({ id: 'ir-' + r.id, grupo: 'Ir para', titulo: r.titulo, fazer: () => nav.ir(r.id) })),
|
||||
{ id: 'ir-rosto', grupo: 'Ir para', titulo: 'Rosto (estilo, formato e cor)', fazer: () => nav.ir('rosto') },
|
||||
{ id: 'ir-historico', grupo: 'Ir para', titulo: 'Histórico de detecções', fazer: () => nav.ir('historico') },
|
||||
...estado.formatos.map((f) => ({ id: 'formato-' + f, grupo: 'Formato dos olhos', titulo: FORMATOS_INFO[f] ?? f, fazer: () => formato(f) })),
|
||||
estado.estop.ativo
|
||||
? { id: 'rearmar', grupo: 'Ações', titulo: 'Rearmar motores', fazer: rearmar }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/** Rotas por hash (#/conversa): o servidor só precisa entregar index.html em "/". */
|
||||
|
||||
export type Rota = 'ao-vivo' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'historico';
|
||||
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto';
|
||||
|
||||
export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
|
||||
{ id: 'ao-vivo', titulo: 'Ao vivo', atalho: 'g v' },
|
||||
{ id: 'eventos', titulo: 'Eventos', atalho: 'g e' },
|
||||
{ id: 'conversa', titulo: 'Conversa', atalho: 'g c' },
|
||||
{ id: 'reacoes', titulo: 'Reações', atalho: 'g r' },
|
||||
{ id: 'movimento', titulo: 'Movimento', atalho: 'g m' },
|
||||
@@ -13,13 +14,13 @@ export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
|
||||
/** Páginas abertas a partir de outra tela (não aparecem na navegação). */
|
||||
export const SECUNDARIAS: { id: Rota; titulo: string; volta: Rota }[] = [
|
||||
{ id: 'rosto', titulo: 'Rosto', volta: 'ao-vivo' },
|
||||
{ id: 'historico', titulo: 'Histórico', volta: 'ao-vivo' },
|
||||
];
|
||||
|
||||
export const tituloDa = (id: Rota) => [...ROTAS, ...SECUNDARIAS].find((r) => r.id === id)?.titulo ?? '';
|
||||
|
||||
function lerHash(): Rota {
|
||||
const id = location.hash.replace(/^#\/?/, '').split('?')[0] as Rota;
|
||||
let id = location.hash.replace(/^#\/?/, '').split('?')[0] as Rota;
|
||||
if ((id as string) === 'historico') id = 'eventos'; // endereço antigo
|
||||
return ROTAS.some((r) => r.id === id) || SECUNDARIAS.some((r) => r.id === id) ? id : 'ao-vivo';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user