374 Commits
Author SHA1 Message Date
Alexandre Possebom 87c4ea32e0 docs: mark deploy done, record kauai deployment facts
Build and Push Docker Image / build (push) Successful in 5m32s
Item 5 of the MVP roadmap shipped on 2026-08-06: the app is live at
gate-watch.possebom.com and the first real plate landed at 12:01 BRT.

Records what the deploy taught, so a future session does not relearn it:
the central Postgres is reached as postgres:5432 (DNS alias on
traefik_proxy) and not as a LAN IP; the Protect webhook must use the LAN
address because the public host answers 403 by design; cameras
auto-register with the device id as location_name.

Also documents why plate_unifi.rs models only the fields the handler
reads — the production rejection that motivated it.
2026-08-06 12:57:55 -03:00
Alexandre Possebom e437bb11e2 fix: tolerate lean UniFi payloads, redact DB log
Build and Push Docker Image / build (push) Successful in 6m24s
A real Alarm Manager POST was rejected in production with
`missing field "group"`. The payload model required nine fields the
handler never reads (conditions, sources, name, eventPath,
eventLocalLink, eventId, group, key, zones), so any absent one made
serde reject the whole JSON and the plate was lost. Model only what is
read — device, timestamp, value, thumbnail, snapshot — and document why
the struct must stay lean. Regression test uses the payload shape
observed on 2026-08-06.

Stop logging the database URL in init_db: it printed the password in
clear text, and startup already logs it via redacted_database_url().

The kauai .env pointed the database at 10.2.0.200, which is no host —
the container crashlooped on first deploy with "No route to host". Use
the postgres:5432 DNS alias on traefik_proxy, like the other stacks.
2026-08-06 12:54:20 -03:00
Alexandre Possebom eb43780306 chore: ignore .planning (session handoffs)
Build and Push Docker Image / build (push) Successful in 6m17s
2026-08-06 11:20:10 -03:00
Alexandre Possebom 9f1ac1197e feat(deploy): kauai stack kit — compose, appsec whitelist, one-shot setup
compose.yaml follows the house convention (traefik_proxy, le resolver,
crowdsec@file, diun); the Protect webhook enters via LAN port 8084 (the
internal-IP guard rejects public client IPs). kauai-setup.sh creates the
gate_watch DB on the central Postgres, writes the .env (prompted token,
generated JWT/DB password), wires the 911100 PUT/DELETE carve-out into the
crowdsec appsec configs (anchored sed, verified against the live compose in
a /tmp sandbox) and brings the stack up with a health smoke test.
2026-08-06 11:16:48 -03:00
Alexandre Possebom 11bdbe3ae4 docs: mark MVP items 2-4 done in the roadmap
Build and Push Docker Image / build (push) Successful in 6m10s
2026-08-06 11:08:40 -03:00
Alexandre Possebom 6eba62ec90 feat: recurring-unknown plate alert over Telegram
An ownerless plate with >=3 passages in the last 7 days fires one Telegram
alert to every user with a telegram_id, then goes silent for 7 days (or until
the vehicle gains an owner). The silence window is an atomic conditional
UPSERT on the new recurring_unknown_alerts table, so concurrent ingests can't
double-alert; verified the claim semantics and the full migration chain on a
throwaway postgres:16. Runtime queries only (no .sqlx entries). Falls back to
a text message when the passage has no photo (new TelegramService::send_message).
2026-08-06 11:08:14 -03:00
Alexandre Possebom 20bbcdd3e0 feat(unifi): first-class unifi protocol, snapshot fallback, payload tests
Auto-registered Protect cameras now get protocol='unifi' (new CHECK value)
instead of a bogus vzenith/cancela row; the camera form offers UniFi Protect
and skips credential requirements for it (webhook-only source). The alarm
image falls back to alarm.snapshot when thumbnail is absent, stripping any
data-URI prefix. Detection carries the source protocol. 3 unit tests cover
the Alarm Manager payload contract.
2026-08-06 11:03:22 -03:00
Alexandre Possebom 0fbc1c09e1 feat!: monitor-shaped dashboard summary, parking/settings removed
SummaryDto becomes today_count / distinct_plates_today / known_today /
unknown_today (runtime queries, no .sqlx entries needed). The parking-spots
setting, its API, page and repository are gone — /settings now redirects to
the profile (only Aparência/Perfil remain).
2026-08-06 10:58:24 -03:00
Alexandre Possebom 205b777323 docs: rewrite CLAUDE.md/CONTEXT.md/AGENTS.md for gate-watch, add README
Build and Push Docker Image / build (push) Successful in 5m53s
CONTEXT.md gets the monitor glossary (Known Vehicle, Alert, Recurring
Unknown; Gate and Whitelist removed). CLAUDE.md records the fork decisions,
the UniFi webhook contract, inherited gotchas and the agreed MVP roadmap.
2026-08-06 10:49:46 -03:00
Alexandre Possebom f1786c6af8 chore!: rebrand to gate-watch
Package/lib/binary renamed (gate_watch crate), SPA branding + storage keys,
docker-compose services/db renamed, Dockerfile drops the lpr-whitelist git
credential. CI keeps build+push (image name follows the repo) and the Telegram
notify, but the prefecture deploy webhook is gone. Prefecture-specific deploy/
configs and historical docs/ removed (git history keeps them).
2026-08-06 10:42:54 -03:00
Alexandre Possebom b42326a899 feat!(spa): remove gate buttons, whitelist sync UI and gate camera fields
Locations lose open/close-gate and resync (the API is gone); the camera form
drops gate_type/relay-channel. Vehicle create/assign no longer polls
whitelist-status — plain success toasts. The is_whitelisted badge is
relabeled Conhecido/Desconhecido (known owner, not authorization).
2026-08-06 10:39:39 -03:00
Alexandre Possebom ef3ab93ae4 feat!: fork gate-control as gate-watch — remove gate control + whitelist sync
gate-watch is a street monitor: it observes plates, it does not act on any
barrier. Deleted the gate API, camera_driver (Vzenith/Dahua gate+whitelist
ops), whitelist queue worker + daily reconcile, and the dead Askama templates.
digest_get moved to utils/digest_http (the Dahua event stream still needs it).
New migration drops whitelist_queue{,_execution} and the enqueue trigger.
Drivers/companies stay as known-owner metadata (enrichment, not authorization).
2026-08-06 10:35:38 -03:00
Alexandre Possebom 1c31046ee1 fix(vehicles): normalize plate on manual create
A plate typed in lowercase went straight from the form to the INSERT,
where the check_plate_format constraint (which only accepts [A-Z])
rejected it with 23514. The handler only maps 23505, so it surfaced as
a 500 "Erro ao acessar banco de dados" and nobody could register a
vehicle from the UI.

Run the input through Plate::parse, the same value type the webhook,
import and reconcile paths already use: it uppercases, strips
separators and validates both Brazilian layouts. A malformed plate is
now a 400 naming the expected format instead of a database error.

The form field uppercases while typing and drops non-alphanumerics, so
what is shown is what gets stored.
2026-08-05 07:53:30 -03:00
Alexandre Possebom 5ccb48fed7 fix(ui): safelist the dynamic font utilities
FontProvider builds the class as `font-${font}`, which Tailwind's
scanner cannot see, so the family utilities were only ever emitted by
accident: `font-plex` and `font-manrope` appear as literal strings in a
doc comment in config/fonts.ts (and font-manrope in one className), and
`font-system` appeared nowhere, so its utility did not exist at all.

Selecting "System" therefore applied a dead class. The rendered result
happened to be correct because Tailwind's preflight already sets
`html { font-family: var(--default-font-family) }`, which resolves to
the system stack, but nothing about that was intentional: defining
--default-font-family or a family on body would have broken the option
silently.

Add the missing --font-system token and an explicit @source inline
safelist next to it. Verified by mutation: removing `font-plex` from
the doc comment and rebuilding still emits all three utilities, so the
safelist is now what generates them rather than the comment.

No visible change; this makes the existing behaviour intentional and
stops a font rename from silently dropping the family.
2026-08-04 18:21:46 -03:00
Alexandre Possebom 4003841b61 refactor: resolve set-state-in-effect lint errors
vehicle-detail seeded the owner toggle from the loaded vehicle inside
an effect, which cost an extra committed render and left the dependency
list out of sync with the body: it read `vehicle` but listed
`vehicle?.company_id` / `vehicle?.driver_id`. Adjust the state during
render against a key built from those two ids instead, which also
clears the exhaustive-deps warning the same effect carried.

Verified equivalent by simulating both versions across eight event
sequences (initial load, user flipping the toggle, assigning a company,
removing the owner, swapping the driver, and no vehicle at all):
identical committed state in every one.

alert-dialog keeps its effect behind a disable comment, with the reason
recorded next to it. That dialog stays mounted between openings because
the parent always renders it with `open` as a prop, so the canonical
`key` remount would not reset the "New -> close -> New" case, and keying
on `open` would remount on close and kill the exit animation.

Lint goes from 5 problems (2 errors) to 2 (0 errors); the remaining two
are pre-existing react-refresh warnings in accent-color-provider.
2026-08-04 18:17:02 -03:00
Alexandre Possebom 4582eb996f feat(ui): switch default typeface to IBM Plex Sans
Inter is visually ubiquitous; IBM Plex Sans is drawn for technical
interfaces, gives the dense tables more character, and keeps full
Latin-Extended coverage for pt-BR.

Rename the font token to 'plex'. fonts[0] is both the default and the
fallback for an unrecognised cookie, so anyone still holding
font=inter lands on the new face without a migration.

Narrow the Google Fonts request to wght@400..700 with no italic axis:
the app uses only 400/500/600/700 and no italic anywhere, while the
previous Inter request pulled 100..900 plus the full italic range.
Manrope stays as the alternative in Settings > Appearance.
2026-08-04 18:05:38 -03:00
Alexandre Possebom 7f380aaff4 fix(a11y): repair skip link, contrast and copy
Findings from a cross-discipline interface review of the primary flow
(sign-in, dashboard, and the vehicle/driver/company lists and details).

The skip link pointed at #content, which no element defined, so the
first focusable control on every authenticated page did nothing. Give
<main> that id and make it focusable.

Hardcoded status colors failed WCAG AA: "Não autorizado" measured
2.02:1 and "Autorizado" 2.99:1 on the dark card, and the white-on-amber
badge 2.15:1 in both themes. Dark --muted-foreground sat at 3.77:1 over
the background, and it carries most of the secondary text. Raising it
to 0.78 clears AA on background and card; over --muted it reaches only
3.88:1, which still covers the icon and kbd uses that land there.

Also declare lang="pt-BR", translate the leftover template strings
(ConfirmDialog, the whole pagination, the skip link), label the three
list search inputs, announce live passages through a polite status
region, gate motion behind prefers-reduced-motion, let the search
inputs adapt instead of sitting at a fixed 150px, distinguish
filtered-empty from truly-empty table states, align the remaining
sentence-case dialog titles with the app's Title Case convention, and
use a real ellipsis character.
2026-08-04 18:05:28 -03:00
Alexandre Possebom eb5da9974a chore(shadcn): unify @ss and wire the UI Kit registry
Shadcn Studio serves its whole catalog from one endpoint, so a single
@ss replaces the split @ss-components / @ss-blocks / @ss-themes trio
that only forced a guess about which catalog an item lived in.

The path segment right after /r/ is the STYLE slot, so it must carry
{style} (which resolves to new-york-v4 here). Omitting it makes the
server read 'components' or 'blocks' as a style name and answer
404 {"error":"Invalid style"} for every item.

Also add @shadcnuikit, the second paid vendor, which was configured in
sibling projects but missing here; its bearer token lives in the
gitignored frontend/.env next to the Studio credentials.
2026-08-01 15:48:57 -03:00
Alexandre Possebom 1b9e803451 docs: replace Heatwave notes with Dahua worker and camera FK 2026-07-17 12:59:48 -03:00
Alexandre Possebom c010ebc355 feat(passages): open full image from passage list thumbnail 2026-07-17 12:59:48 -03:00
Alexandre Possebom 4bc7614292 fix: resolve confirmed bugs from bug hunt
Fixes verified by the 3-agent adversarial bug hunt (hunter, skeptic,
referee). Grouped by area:

Security
- routes: webhook internal-network check now reads the real client IP
  (rightmost X-Forwarded-For hop, same as the rate limiter) so the
  unauthenticated /api/plate and /api/unifi are no longer reachable
  from the internet through Traefik
- config: mask the DB password in the startup log
- frontend: remove the dead /sign-up template that logged the
  plaintext password to the console

Correctness and data integrity
- passage_ingest: serialize ingestion per plate to close the debounce
  check-then-act race (duplicate passages and duplicate alert fan-out)
- api_vehicles: delete now removes the plate's images from disk (were
  left orphaned) and the delete dialog warns about history/image loss
- passagem_repository: cast plate params to bpchar[] to keep the index
  on the hot passages queries
- camera_repository: reject out-of-range ids instead of truncating i64
  to i32, which could act on the wrong camera
- api_vehicles: reject create with both a driver and a company
- api_users: block an admin from removing their own admin flag
- api_dashboard: clamp available spots to [0, total] and propagate DB
  errors instead of reporting capacity as 0
- api_alerts: map FK violation (23503) to 404 instead of an opaque 500
- api_profile: validate that the email contains '@'
- api_drivers: measure name length by chars, not bytes
- dahua: fail list_plates on firmware truncation so reconcile flags the
  camera instead of diffing a partial list
- image: passage thumbnails now honor the per-camera crop_percent
- dashboard: keep the live feed bounded instead of growing forever

Regenerated .sqlx for the changed queries and routeTree.gen.ts.
2026-07-16 10:23:32 -03:00
Alexandre Possebom dfc4256676 feat(dashboard): infinite scroll on passages feed
Replace the "Carregar mais" button with an IntersectionObserver
sentinel that triggers loadMore 200px before the bottom, with an
in-flight guard against duplicate page fetches.

Live SSE prepends no longer trim the list below what the user has
scrolled to load: trimming past the cap dropped on-screen items and
skewed nextOffset, skipping pages on the next fetch.
2026-07-15 11:34:26 -03:00
Alexandre Possebom 5132897d2e refactor(alerts): Notifier seam for the fan-out
AlertService::process_alerts branched per channel with three copy-pasted
spawn/format/log blocks and threaded all three channel configs through a
6-arg signature. Replace it with a Notifier trait (services/notify/,
mirroring camera_driver) and three thin adapters wrapping the existing
Telegram/WhatsApp/Email transports. Each adapter decides whether the
recipient enabled its channel and has the contact, so the fan-out is
channel-agnostic and process_alerts shrinks to (&AppState, passage,
image). A new channel is one adapter and zero edits to the orchestrator.

Extract the byte-identical format_relative_time into utils. Add a fan_out
unit test via a fake Notifier (no DB, no network).

Behavior unchanged: same channels, messages, and fire-and-forget sends.
2026-07-14 13:54:01 -03:00
Alexandre Possebom 100f040474 refactor(passages): unify intake into one module
The plate webhook, the UniFi webhook, and the Dahua event stream each
re-implemented the same pipeline (debounce, whitelist, persist, image,
broadcast, alerts). Extract one deep ingest(Detection) module; the three
sources become thin adapters that build a Detection.

Fix UniFi: it skipped debounce and whitelist entirely, so its passages
were never deduped and never whitelist-flagged. The module runs both for
every source.

The image is a lazy provider awaited only after debounce, so the Dahua
snapshot fetch no longer runs on a debounced duplicate. Thumbnails are
unified to 400px sync; a bad webhook image degrades to no-image, not 500.

Add integration tests at the ingest interface (debounce dedup, whitelist
flag).
2026-07-14 13:32:44 -03:00
Alexandre Possebom 66ff35efd0 refactor(plate): unify parsing into a value type
Collapse PlateValidator (runtime) and plate_normalizer (import) into one
Plate value type with parse/parse_many. Parse at each ingestion edge
(webhook, unifi, dahua worker, import) and pass the normalized &str
downstream, so a plate cannot exist un-normalized.

Fix whitelist reconcile: it kept its own trim+upper normalize that did
not strip separators, so "ABC-1D23" and "ABC1D23" compared unequal and
produced spurious add/remove diffs against the camera. Both sides now go
through Plate::parse.

Add CONTEXT.md domain glossary, leading with Plate.
2026-07-14 13:03:35 -03:00
Alexandre Possebom b271438dca feat(cameras): report whitelist sync on authorize
After authorizing a plate the SPA now reports whether it reached the
registered cameras, instead of a generic success toast.

- Add GET /api/vehicles/{plate}/whitelist-status: the latest whitelist
  queue op for the plate plus its per-camera execution status and the
  registered-camera count (runtime queries, no migration).
- Poll it after create/assign-driver/assign-company and drive a single
  evolving toast: loading -> "adicionada em N cameras" / "falha em X" /
  still-syncing timeout.
2026-07-14 10:02:37 -03:00
Alexandre Possebom 873094d21a feat(cameras): tipo de barreira (portão/cancela) por câmera
Campo gate_type por câmera (default cancela) que dirige os rótulos da
UI: botões "Abrir/Fechar portão" vs "Abrir/Fechar cancela" e toasts com
concordância de gênero. Sem mudança de comportamento — o relé é o mesmo.
2026-07-13 14:25:23 -03:00
Alexandre Possebom 492dbb72bf style(cameras): botão "Fechar cancela" em vermelho 2026-07-13 14:14:15 -03:00
Alexandre Possebom 46d51b1aac feat(cameras): whitelist reconciliation for Dahua
Make a Dahua camera's whitelist match the authorized set (vehicles
with a driver or company owner): remove plates that aren't authorized
and add the ones that are missing.

- CameraDriver::list_plates reads the camera whitelist (Dahua via
  recordFinder find; Vzenith unsupported, so it's skipped).
- POST /api/cameras/{id}/resync + a per-Dahua-camera "Ressincronizar"
  button (confirm dialog, toast with removed/added counts).
- Daily worker at 08:00 BRT reconciles every Dahua camera and emails
  admins only when something changed or a camera couldn't be reached.
2026-07-13 13:36:34 -03:00
Alexandre Possebom 14f4891943 feat(cameras): sentido fixo (entrada/saída) configurável por câmera
Câmeras Dahua não informam entrada/saída, então onde só há câmeras Dahua
(unidade SIMOT) a contagem de entradas/saídas do dashboard ficava zerada
mesmo com as placas sendo lidas.

Adiciona `fixed_direction` (in|out|NULL) por câmera, configurável no
formulário do front. NULL = automático (Vzenith deriva do payload, Dahua
sem sentido); quando setado, sobrepõe o payload em toda passagem da câmera.

- migration + coluna com CHECK (in|out)
- Camera / CameraRepository / api_cameras (input + validação 400)
- dahua_event_worker e webhook Vzenith passam a honrar o sentido fixo
- front: Select Automático/Entrada/Saída no cadastro de câmera
- ajusta os callers de CameraRepository::create/update nos testes
2026-07-13 10:35:49 -03:00
Alexandre Possebom 31caaaafe7 feat(cameras): permite excluir câmera com passagens (cascata + aviso na UI)
- delete não bloqueia mais quando há passagens; CameraRepository::remove
  apaga em transação as passagens e whitelist_queue_execution do IP e então
  a câmera (resolve as FKs ON DELETE RESTRICT)
- list/get/create/update passam a retornar CameraDto com passage_count
- UI: dialog de exclusão avisa quantas passagens serão apagadas (plural correto)
2026-07-09 15:02:46 -03:00
Alexandre Possebom 02e355c058 docs(deploy): registra achados do rollout SIMOT (traefik subdir-watch, crowdsec sem restart) 2026-07-09 14:31:30 -03:00
Alexandre Possebom 1279df67fc feat(deploy): segunda instância SIMOT (deploy.sh + traefik + docs) 2026-07-09 14:13:20 -03:00
Alexandre Possebom e9f9ba8260 docs(deploy): plano de implementação da segunda instância (SIMOT) 2026-07-09 13:56:28 -03:00
Alexandre Possebom df9ff5980e docs(deploy): design da segunda instância (SIMOT) 2026-07-09 13:52:57 -03:00
Alexandre Possebom cd1f615ff7 feat(drivers): mostra última passagem de cada veículo no detalhe do motorista
- backend: DriverVehicleItem ganha last_passage (Option<DateTime<Utc>>),
  populado do timestamp da última passagem (já vinha do
  get_last_passages_for_plates, só o thumbnail era usado).
- frontend: driverVehicleSchema + last_passage; cada veículo na lista do
  motorista mostra 'Última passagem dd/MM/yyyy HH:mm' (ou 'Sem passagens')
  com ícone de relógio.
2026-07-09 08:52:23 -03:00
Alexandre Possebom 86751af677 feat(drivers): thumb maior + veículo clicável leva à página do veículo
Na página de detalhe do motorista o thumb dos veículos era size-9
(36px quadrado) e a linha não era clicável. Agora:

- thumb vira h-12 w-20 (paisagem, do tamanho da lista de veículos);
- placa + thumb envolvidos num Link pra /vehicles/$plate (clique leva
  à página do veículo), com hover bg + underline na placa;
- botão de remover segue separado (não navega).
2026-07-09 08:27:35 -03:00
Alexandre Possebom ecc9618fbc chore(log): tracing em formato compact (uma linha por evento)
pretty() jogava o contexto de span do TraceLayer (ip/method/uri) numa
linha indentada separada, dobrando cada log de request. compact() colapsa
tudo numa linha só, mantendo o timestamp custom.
2026-07-09 08:05:59 -03:00
Alexandre Possebom ab840ccf0b fix(cameras): MasterOfCar truncado a 15 bytes (limite do firmware Dahua)
O firmware Dahua/Intelbras rejeita MasterOfCar > 15 bytes com HTTP 400
'Bad Request' (verificado empiricamente direto no CGI: 15=ok, 16=400).
Nomes de proprietário longos (>15 bytes) quebravam toda insert.

Agora o nome é truncado a 15 bytes em fronteira de char UTF-8 antes de
URL-encode: nome curto vai inteiro, longo vai truncado (melhor que nada).
removeEx+insert seguem idempotentes.

Testes: truncamento por byte + fronteira UTF-8 ('João') + nome longo.
2026-07-09 07:47:20 -03:00
Alexandre Possebom 7cea8ac9da fix(cameras): note da whitelist Vzenith volta a ser ASCII
Regressão do 96544fb: a note virou 'Proprietário: {nome}' (não-ASCII),
mas o protocolo binário Vzenith exige ASCII -> 'protocol error: note
must be ASCII' em toda câmera Vzenith. O nome do proprietário só é
relevante na Dahua (MasterOfCar); a Vzenith não tem campo de dono, então
a note volta ao label ASCII fixo e owner_name é ignorado lá.
2026-07-09 07:29:30 -03:00
Alexandre Possebom 89dc4f115e feat(worker): loga progresso da fila de whitelist (item/total + %)
Cada item processado agora mostra '— {done}/{total} ({pct}%) done' nas
linhas de completed/failed. O total é o tamanho do lote pendente busado
no início do ciclo, então num backfill de N placas vê-se 1/N..N/N.
'O Found N' debug virou INFO pra dar o total do lote logo de cara.
2026-07-08 16:58:15 -03:00
Alexandre Possebom 96544fbcc9 feat(cameras): whitelist envia nome do proprietário (MasterOfCar Dahua)
O add_plate só enviava a placa; a entrada da whitelist ficava sem o nome
do dono. Agora o campo MasterOfCar da TrafficRedList (Dahua) é populado.

- CameraDriver::add_plate ganha owner_name: Option<&str>
- DahuaDriver: anexa &MasterOfCar=<urlencoded> quando há dono (nomes têm
  espaços/acentos). Omitido quando None (sem dono).
- VzenithDriver: usa o nome na note da placa.
- WhitelistQueueWorker: busca o proprietário (motorista OU empresa) por
  placa e repassa ao driver.
- find_driver_name -> find_owner_name (COALESCE driver/company); alinha
  com o fix D8 ('Proprietário'). Também popula o nome do dono exibido na
  passagem de veículos de empresa (antes vinha vazio).

clippy -D warnings verde; 183 testes --lib ok.
2026-07-08 16:54:20 -03:00
Alexandre Possebom 329b19a511 chore(lint): resolve dead_code para make lint verde (-D warnings)
O lint while_let_loop (commit anterior) mascarava 11 dead_code que o clippy
do target bin não rastreia: código usado por templates Askama e por testes
de integração (compilam o lib como dep externa).

Removidos (genuinamente mortos):
- DriverRepository::get_all
- EmailService::send_welcome_email
- WhatsappService::send_text / send_text_with_retry
- Company::formatted_updated_at (sem template consumindo)

#[allow(dead_code)] justificado:
- Camera/Driver formatted_*, Passage::formatted_datetime, PlateChar/placa_chars
  -> renderizados por templates .html.j2 (código Askama não rastreado)
- PassageRepository::{find_by_id,delete_by_id,find_recent,
  get_last_passage_time,find_by_plate_paginated}, VehicleRepository::list_all
  -> usados por testes de integração

cargo clippy -- -D warnings agora verde; 182 testes --lib ok.
2026-07-08 16:44:17 -03:00
Alexandre Possebom df350edebd fix(cameras): drain_parts usa while let (clippy while_let_loop)
Loop manual com 'let Some(..) else { break }' vira while let; o segundo
ponto de saída (part incompleta, com drain antes do break) permanece como
let-else interno. Comportamento idêntico, só satisfaz o lint novo do
toolchain 1.96.
2026-07-08 16:34:11 -03:00
Alexandre Possebom ab0a14a533 fix(cameras): backfill da whitelist ao cadastrar câmera nova
Câmeras cadastradas depois dos veículos já autorizados (ex.: a
Intelbras/Dahua) ficavam sem a lista de permissões: o trigger
vehicle_driver_whitelist_trigger só dispara em UPDATE de
driver_id/company_id, então a whitelist existente nunca era
sincronizada à câmera nova. Mesma classe do bug B3.

- migration 20260708000002: backfill único enfileirando ADD para
  todo veículo com dono (idempotente via NOT EXISTS).
- CameraRepository create handler: ao criar câmera nova, chama
  enqueue_backfill_for_owned_vehicles para retro-sincronizar
  (evita reincidência).
- testes de integração cobrem o backfill e a idempotência.

Validado em Postgres descartável: veículo c/ dono e só ADD concluído
é reenfileirado; ADD pendente não duplica; empresa entra; sem dono
ignorado; re-run é no-op.
2026-07-08 16:28:47 -03:00
Alexandre Possebom 8d7bdcf645 fix(images): thumbnail usa fatores de escala reais do TurboJPEG (3/16 e 5/16 não são suportados) 2026-07-08 13:29:17 -03:00
Alexandre Possebom c33ec001b5 fix(cameras): snapshot Dahua usa channel=1 (channel=0 → HTTP 400) 2026-07-08 13:10:47 -03:00
Alexandre Possebom af51b124d5 Merge branch 'feat/dahua-camera-integration' 2026-07-08 10:03:07 -03:00
Alexandre Possebom 5fa9c7189e fix(cameras): worker recarrega câmera na reconexão + loga part que não parseia; relé volta a 0 ao sair de dahua 2026-07-08 09:52:57 -03:00
Alexandre Possebom 2eac1c66f3 test(cameras): cover per-camera creds + dahua fields; docs 2026-07-08 09:37:29 -03:00