Dogwalk — updates e próximos passos
🐶 Dogwalk·

Dogwalk — updates e próximos passos

📖 8 min de leitura← Voltar para timeline

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:

  1. 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.

  2. 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.

  3. 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.