No terreno, nem sempre há rede
Caves, salas técnicas, coberturas: um técnico tem de conseguir registar horas, tirar notas e fotografias sem ligação. A app móvel, em Flutter, escreve por isso cada ação numa base de dados local, numa fila de envio (outbox), e sincroniza-a por lotes assim que a rede volta. A base local serve para trabalhar offline; o servidor continua a ser a fonte de verdade.
Os eventos sincronizados cobrem toda a intervenção:
- chegada, início, pausa, retoma e fim do trabalho;
- notas e medições estruturadas;
- peças utilizadas ou indisponíveis;
- fotografias e assinatura do cliente;
- cliente ausente, acesso bloqueado, perigo, nova visita necessária, colocação em segurança.
Um identificador por evento
Cada evento recebe no telemóvel um clientEventId (UUID v4), que nunca é gerado de novo numa nova tentativa. Repetir exatamente o mesmo evento não tem qualquer efeito, mesmo depois do encerramento da intervenção: a resposta limita-se a assinalá-lo como duplicado. Reutilizar o mesmo identificador com outro conteúdo é recusado (409).
A app pode assim reenviar um lote inteiro depois de perder uma resposta de rede, sem risco de duplicados. Um lote tem no máximo 100 eventos e é transacional: se um deles for inválido, nenhum é criado. O servidor controla também a sequência dos registos de horas; uma pausa sem início ou dois inícios seguidos são rejeitados. Uma peça declarada só desconta o stock da viatura uma vez, mesmo que o envio seja repetido.
Ficheiros verificados antes de serem aceites
Antes de enviar uma fotografia ou uma assinatura, a app declara uma intenção de envio: tipo MIME, tamanho e impressão digital SHA-256. O servidor responde com um URL que inclui um token aleatório, válido durante quinze minutos e de utilização única.
Na receção, verifica o token, o tamanho, a impressão digital e a assinatura binária real do ficheiro. Só um ficheiro validado, que pertença à mesma organização e à mesma intervenção, pode depois ser referido por um evento no terreno. Os ficheiros vão para um armazenamento de objetos (S3 ou compatível, como o MinIO), e não para o PostgreSQL.
Um fluxo em tempo real que retoma onde parou
Os ecrãs de planeamento e a app recebem as alterações através de um fluxo SSE autenticado e isolado por organização. Após uma falha, o cliente retoma a partir do último evento recebido graças ao cabeçalho Last-Event-ID; os eventos são conservados sete dias por predefinição. Um técnico só recebe os das intervenções que lhe estão atribuídas.
As notificações push (Firebase Cloud Messaging) são propositadamente genéricas: só transportam identificadores, e a app volta sempre a carregar os dados de negócio através da API autenticada.
O que a idempotência muda
Coloca a complexidade no sítio certo. A app não precisa de saber se um envio anterior chegou: na dúvida, reenvia. O servidor garante que o mesmo facto só é registado uma vez.


