
Dogwalk — updates e próximos passos
Contexto
O Dogwalk (PataPass) já não é mais aquele MVP de maio. Desde o último post dedicado, o projeto ganhou contornos de plataforma de verdade — geofencing, multi-cidade, tema por perfil, e uma qualidade de build que me orgulha. Esse post é um checkpoint: o que foi feito, o que aprendi, e o que vem por aí.
A stack continua sendo FastAPI + PostgreSQL + React 19 + Vite 8, mas o ecossistema ao redor amadureceu — systemd service, Cloudflare Tunnel, CI/CD com testes E2E, build guard contra imports quebrados.
Geofencing — o mapa virou cérebro
A maior novidade de julho foi o sistema de geofencing completo. Começou como uma ideia modesta — “mostrar a área de atendimento do passeador” — e virou um módulo inteiro com três componentes:
ServiceAreaDrawer
Cada passeador pode desenhar suas áreas de atendimento no mapa: círculo arrastável, slider de raio, múltiplas áreas por perfil. Salvo no backend via REST, carregado na busca.
// ServiceAreaDrawer — arrasta, redimensiona, salva
function ServiceAreaDrawer({ profileId }: Props) {
const [areas, setAreas] = useState<ServiceArea[]>([]);
const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => {
setIsSaving(true);
await api.put(`/profiles/${profileId}/service-areas`, { areas });
setIsSaving(false);
toast.success('Área de serviço atualizada!');
};
return (
<div className="space-y-4">
<MapContainer>
{areas.map((area, i) => (
<DraggableCircle
key={i}
center={area.center}
radius={area.radius}
onChange={(updated) => updateArea(i, updated)}
/>
))}
</MapContainer>
<Slider
value={radius}
min={500}
max={10000}
onChange={setRadius}
label={`Raio: ${(radius / 1000).toFixed(1)} km`}
/>
<Button onClick={handleSave} loading={isSaving}>
Salvar áreas
</Button>
</div>
);
}
A parte mais desafiadora foi o useCallback dance com Rolldown (Vite 8). Inline closures com block body em props JSX quebram o build — precisei extrair handlers nomeados.
Proximity Alerts
Quando o passeador está numa walk e passa perto (< 50m) de um ponto de interesse (POI), o app avisa automaticamente:
📍 {"lat": -23.587, "lng": -46.657} — Próximo ao Portão 7 (Ibirapuera)
📢 "Você está passando perto de um ponto de interesse!"
Os POIs são reais — 33 pontos seedados na região do Ibirapuera, com nomes, coordenadas e categorias. A lógica de proximidade roda no frontend durante a walk via GPS tracking, sem latência de servidor.
// Proximity check — eficiente, sem biblioteca extra
function checkProximity(
currentPos: [number, number],
pois: MapPoint[],
threshold = 50 // metros
): MapPoint[] {
return pois.filter(poi => {
const dist = haversineDistance(currentPos, [poi.lat, poi.lng]);
return dist <= threshold;
});
}
Testes de migração
O módulo de geofencing veio com testes de migração que validam dados seedados. Aprendi na prática que updated_at precisa ser explícito no SQL de seed, e que act() do React Testing Library pode causar timeout se usado em volta de Promises que resolvem rápido demais.
Multi-cidade — preparando o terreno
Antes, a busca era ingênua — trazia todos os passeadores, independente de onde estavam. Agora tem dois novos routers no backend:
GET /cities → lista todas as cidades com walkers
GET /cities/{city}/walkers → walkers filtrados por cidade
GET /search/walkers → busca unificada: city + price + rating
A implementação seguiu o padrão que já estava funcionando em outros endpoints: get_optional_user para endpoints públicos, ordenação por rating descendente com nulls last, paginação via limit/offset.
A maior surpresa veio com o SQLModel: select(func.count()).select_from(query.subquery()) é o padrão pra contar total de resultados ignorando paginação. Parece óbvio em retrospecto, mas me custou uma hora de debug até achar a sintaxe certa.
Tema por perfil — identidade visual própria
Um dos feedbacks que mais ouvi nos testes foi: “tutor e passeador parecem o mesmo app”. Resolvi com tema dinâmico por perfil:
| Perfil | Cor | Sensação |
|---|---|---|
| Tutor 🐕 | Laranja (#f97316) | Acolhedor, dono |
| Passeador 🦮 | Verde (#22c55e) | Natureza, confiança |
Na prática, é um CSS custom properties swap no login:
:root {
--brand: var(--tutor);
}
body[data-role="walker"] {
--brand: var(--walker);
}
O shadcn/ui foi completamente subjugado — as cores do sistema agora vêm das brand vars, não da paleta default. Foi mais trabalho do que esperava (cada componente shadcn tem referências hardcoded a primary, ring, background), mas o resultado final ficou consistente.
Build guard — chega de surpresas no deploy
Quantas vezes um build quebrou no CI por causa de um import quebrado que eu só descobria depois de 3 minutos de pipeline? Muitas. A solução foi um plugin Vite que quebra o build explicitamente se detectar UNRESOLVED_IMPORT:
// vite.config.ts — build guard
function unresolvedImportGuard(): Plugin {
return {
name: 'unresolved-import-guard',
buildEnd(error) {
if (error?.message?.includes('UNRESOLVED_IMPORT')) {
console.error('[BUILD GUARD] Import não resolvido detectado!');
process.exit(1);
}
},
};
}
Parece simples, mas o impacto foi enorme — zero builds quebrados em CI desde que implementei.
O que vem por aí
O próximo ciclo tem 3 frentes:
-
PetProfile + WalkerProfile pages — já começou na branch
feature/geofencing, com rotas, navegação e conexão com API. Vai unificar a experiência de edição de perfil. -
Notificações push — o WebSocket de notificações tá pronto no backend, mas o frontend ainda não integra com Service Worker pra notificações nativas. Prioridade alta.
-
Programa de indicação — o componente ReferralCard já existe nos dashboards, mas a lógica de tracking + recompensa ainda não tá amarrada com Stripe Connect.
Além disso, quero dedicar tempo pra documentação de onboarding — o fluxo de cadastro de passeador ainda tem gaps que geram dúvidas nos testes.
Aprendizados
- Geofencing é mais complexo do que parece. Não é só “calcular distância” — é desenhar áreas, persistir, carregar, atualizar em tempo real, tratar bordas de mapa.
- Multi-cidade prepara o terreno pra escala. Ter cidades, busca e filtros agora significa que quando chegar a hora de expandir pra outros estados, a infra já tá pronta.
- Tema por perfil não é só visual. É uma decisão de produto que comunica “esse app é seu” pra cada tipo de usuário. Vale o esforço.
- Build guard devia ser default. Toda pipeline ganha quando você falha rápido.
16 de julho de 2026. O Dogwalk não é mais um MVP. É uma plataforma. E ainda tem muito chão pela frente.