- "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>
86 lines
2.6 KiB
TypeScript
86 lines
2.6 KiB
TypeScript
/** Histórico de detecções (/historico/*): recarrega ao abrir e quando o servidor avisa por SSE. */
|
|
import { lerToken, pedir, postar } from './api';
|
|
import { avisos } from './avisos.svelte';
|
|
import { ouvir } from './estado.svelte';
|
|
|
|
export interface EventoHistorico {
|
|
id: number;
|
|
horario: number;
|
|
tipo: 'pir' | 'movimento' | 'som';
|
|
evento: string;
|
|
nivel: number | null;
|
|
foto: boolean;
|
|
}
|
|
|
|
export interface StatusHistorico {
|
|
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;
|
|
max_eventos: number;
|
|
max_fotos: number;
|
|
}
|
|
|
|
export const TIPOS_EVENTO: Record<string, { nome: string; emoji: string; cor: string }> = {
|
|
pir: { nome: 'Presença (PIR)', emoji: '🚶', cor: '#34c759' },
|
|
movimento: { nome: 'Movimento na câmera', emoji: '👀', cor: '#0a84ff' },
|
|
som: { nome: 'Som alto', emoji: '🔊', cor: '#ff9500' },
|
|
};
|
|
|
|
/** URL da foto com o token na query (a tag <img> não manda cabeçalho). */
|
|
export function urlFoto(id: number) {
|
|
const t = lerToken();
|
|
return `/historico/foto/evento-${id}.jpg` + (t ? '?t=' + encodeURIComponent(t) : '');
|
|
}
|
|
|
|
class Historico {
|
|
status = $state<StatusHistorico | null>(null);
|
|
private espera = 0;
|
|
|
|
async carregar() {
|
|
try {
|
|
this.status = await pedir<StatusHistorico>('/historico/status');
|
|
} catch (e) {
|
|
avisos.erro(e);
|
|
}
|
|
}
|
|
|
|
async configurar(parcial: Partial<StatusHistorico['config']>) {
|
|
const antes = this.status ? { ...this.status.config } : null;
|
|
if (this.status) this.status = { ...this.status, config: { ...this.status.config, ...parcial } };
|
|
try {
|
|
this.status = await postar<StatusHistorico>('/historico/config', parcial);
|
|
} catch (e) {
|
|
if (this.status && antes) this.status = { ...this.status, config: antes };
|
|
avisos.erro(e);
|
|
}
|
|
}
|
|
|
|
async apagar(id: number) {
|
|
try {
|
|
await postar('/historico/apagar', { id });
|
|
} catch (e) {
|
|
avisos.erro(e);
|
|
}
|
|
}
|
|
|
|
async limpar() {
|
|
try {
|
|
await postar('/historico/limpar');
|
|
avisos.ok('Histórico apagado.');
|
|
} catch (e) {
|
|
avisos.erro(e);
|
|
}
|
|
}
|
|
|
|
/** Vários avisos seguidos (evento + foto) viram uma recarga só. */
|
|
agendar() {
|
|
clearTimeout(this.espera);
|
|
this.espera = window.setTimeout(() => this.carregar(), 300);
|
|
}
|
|
}
|
|
|
|
export const historico = new Historico();
|
|
ouvir('historico', () => historico.status && historico.agendar());
|