Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com> Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com> implementacion de login, vistas, correcion de errores en vista registro, domicilios
19 KiB
try { final response = await dio.post('/auth/register', data: {...}); } on DioException catch (e) { // Aquí puedes ver el JSON real que envía tu backend en e.response?.data print('Error del backend: ${e.response?.data}'); }
Contexto del Proyecto — Sistema de Recolección Inteligente y Privada de Residuos
Documento para compartir con asistentes de IA (Claude, ChatGPT, etc.)
Última actualización: Mayo 2026
Equipo: 4 personas, hackathon, stack principal Flutter + FastAPI
1. ¿Qué estamos construyendo?
Una app móvil (Flutter) que notifica a los ciudadanos cuándo llegará el camión recolector a su domicilio sin revelar la ubicación del camión en tiempo real ni mostrar el mapa completo de la ruta. Incluye educación sobre separación de residuos mediante una mascota de IA y gestión de cambios operativos (fallas mecánicas, reasignaciones de rutas). Tres roles: ciudadano, chofer, admin.
El problema que resuelve
La gente no sabe cuándo pasará el camión → saca la basura demasiado temprano/tarde → problemas de salud pública. La solución obvia (GPS en vivo del camión) es un riesgo de seguridad y uso inadecuado. Nuestra app balancea información útil con privacidad operativa.
Alcance del MVP
- 7 colonias de Celaya, Gto. mapeadas a 6 rutas (datos provistos en JSON).
- Notificaciones push en 3 momentos: ruta inicia, camión cerca (~15 min), servicio finalizado.
- Validación de domicilio con OCR de recibo (luz/agua) que se borra tras extraer la dirección.
- Mascota IA interactiva + guía offline de separación de residuos.
- Feedback ciudadano + quiz post-recolección.
- Panel admin con mapa (única vista que ve coordenadas) y gestión de reasignaciones.
2. Reglas innegociables (Privacidad por Diseño)
Estas son las prohibiciones del reto convertidas en requisitos de código. Cualquier feature que las viole se rechaza:
- NUNCA se devuelven coordenadas del camión al ciudadano. Solo texto:
"Llega en ~15 min"/"7:20–7:35 p.m.". - NUNCA hay un mapa con el camión moviéndose en la vista ciudadana (prohibido el route tracker).
- Visión de túnel: el ciudadano solo ve su ruta asignada. Cero visibilidad de colonias vecinas u otros usuarios (anti-snooping).
- La ubicación del camión solo existe en el panel admin (para troubleshooting).
- Mensajería preventiva: textos que desalientan sacar basura fuera de horario o perseguir la unidad.
- Quejas hacia la unidad no exponen la identidad del chofer (solo "unidad 101"; el admin sí ve quién la conduce).
- Los recibos (validación de domicilio) no se almacenan crudos: se procesan, se valida, se guarda solo
verified=truey se borra la imagen.
Implementación técnica del túnel:
- Backend: Row Level Security (RLS) de Supabase — el ciudadano físicamente no puede leer
route_positionsni domicilios ajenos aunque evada la API. - Frontend: notificaciones FCM por topic de ruta (
topic_RUTA-01) — cada ciudadano se suscribe solo al topic de surouteId.
3. Stack tecnológico (y por qué)
| Capa | Tecnología | Razón |
|---|---|---|
| Frontend móvil | Flutter + Riverpod (estado) + dio (HTTP) + go_router |
Multiplataforma iOS/Android con un solo código. Riverpod evita polling excesivo. |
| Backend / API | Python + FastAPI | REST async, rápida de escribir, separa lógica de negocio de datos. |
| Base de datos | PostgreSQL (vía Supabase); PostGIS opcional | El mapeo colonia→ruta es directo (JSON), así que no necesitamos point-in-polygon. PostGIS solo si quieren validar por polígono en el futuro. |
| Auth + RBAC | Supabase Auth (JWT) + Row Level Security (RLS) | RLS aplica el túnel a nivel de BD, no solo en código — aunque alguien evada la API, la BD niega el acceso. |
| Almacenamiento | Supabase Storage | Para subir recibo temporalmente. Se borra tras validación. |
| Simulación | APScheduler (FastAPI cron job) | Avanza positionId de cada ruta (1→8) y dispara notificaciones. No hace falta cálculo geoespacial. |
| Push | Firebase Cloud Messaging (FCM) | Asíncrono, ligero, integración nativa con Flutter. |
| Mascota IA | API de Claude/Gemini con system prompt sobre separación | El chat educativo. Guía estática (JSON local) funciona offline. |
| OCR validación | Tesseract o modelo de visión en backend | Extrae dirección del recibo, compara, descarta imagen. |
| Deploy | Cloud Run / Render (backend scale-to-zero) + Supabase | Escala a cero en valles de inactividad. |
4. Datos provistos (la base real del proyecto)
Nos entregaron 3 archivos JSON que definen el modelo y simplifican la implementación:
rutas.json (15 rutas, usamos 6)
- Cada ruta tiene un
routeId(ej."RUTA-01"),truckId(101–115), y 8 posiciones fijas (positionId1→8) con lat/lng, velocidad y timestamp. - Todas salen y regresan al Relleno Sanitario (
20.5111, -100.9037) — posición 1 = posición 8. - Velocidad = 0 en posición 5 (punto más lejano / vuelta).
- Estas coordenadas solo se usan en el panel admin — nunca se mandan al ciudadano.
notificaciones.json (motor de eventos)
Define los 3 eventos que NO se calculan por distancia, sino por cambio de positionId:
| Evento | Se dispara cuando | Mensaje (resumen) |
|---|---|---|
ROUTE_START |
positionId pasa de 1 → 2 |
Salió del relleno rumbo a tu sector |
TRUCK_PROXIMITY |
positionId llega a 4 |
A menos de 15 min; saca tus bolsas |
ROUTE_COMPLETED |
positionId llega a 8 |
Servicio del día finalizado |
colonias-rutas.json (el puente colonia → ruta)
Mapea cada colonia a un routeId + horario. Esto es el "túnel": ciudadano → colonia → routeId → solo eventos de ESA ruta.
Alcance del MVP (7 colonias / 6 rutas):
| Colonia | routeId | Turno |
|---|---|---|
| Zona Centro | RUTA-01 | Matutino |
| Las Arboledas | RUTA-01 | Matutino |
| San Juanico | RUTA-03 | Matutino |
| Los Olivos | RUTA-04 | Matutino |
| Rancho Seco | RUTA-05 | Vespertino ← única vespertina; úsenla para demostrar reasignación |
| Las Insurgentes | RUTA-12 | Matutino |
| Trojes | RUTA-13 | Matutino |
En el registro, el ciudadano elige su colonia de un dropdown (no geocodificación). Eso le asigna su routeId y resuelve el túnel sin PostGIS.
5. Modelo de datos (jerarquía: Usuario → Domicilio → Colonia → Ruta)
users (id, email/phone, role: 'citizen'|'driver'|'admin')
addresses (id, user_id, label, calle, colonia, route_id,
verified BOOL, verified_method, verified_at)
colonias (id, nombre, route_id, horario_estimado, turno)
routes (id 'RUTA-01'..'RUTA-13', name, truck_id,
turno: 'matutino'|'vespertino',
status: 'pendiente'|'en_ruta'|'completada'|'diferida'|'reasignada',
current_position_id INT) -- el "avance" simulado vive aquí (1-8)
route_positions (route_id, position_id, lat, lng, speed, ts)
-- de rutas.json; ⚠️ SOLO panel admin (RLS)
units (id 101.., plate, status) -- 1:1 con driver y con route
drivers (id, user_id, unit_id) -- cada chofer = 1 unidad
collection_events(id, route_id, address_id, status, collected_at)
feedback (id, user_id, address_id, type, target_unit_id,
message, rating, created_at) -- target = unidad, NO chofer
notifications (id, user_id, route_id, type, payload, sent_at)
route_changes (id, route_id, from_unit, to_unit, reason, created_at)
Cambio clave: el estado del camión NO es una coordenada calculada en vivo, sino el campo current_position_id (1–8). Las coordenadas de route_positions solo las consume el panel admin.
6. Cómo funciona la simulación (el corazón del sistema)
La simulación NO calcula distancias ni ETA por geolocalización. Es un índice que avanza + disparadores de eventos.
El cron job (APScheduler en FastAPI)
# El "estado" de cada ruta es solo en qué positionId va (1-8).
estado = { "RUTA-01": 1, "RUTA-03": 1, "RUTA-04": 1,
"RUTA-05": 1, "RUTA-12": 1, "RUTA-13": 1 }
def tick(): # corre cada N segundos (APScheduler)
for route_id, pos in estado.items():
if pos < 8:
nueva = pos + 1
estado[route_id] = nueva # = routes.current_position_id en BD
disparar_si_aplica(route_id, pos, nueva)
def disparar_si_aplica(route_id, antes, ahora):
# Lee notificaciones.json y dispara según el cambio de positionId
if antes == 1 and ahora == 2: push(route_id, "ROUTE_START")
elif ahora == 4: push(route_id, "TRUCK_PROXIMITY")
elif ahora == 8: push(route_id, "ROUTE_COMPLETED")
El túnel vía FCM topics
# La push solo va a quienes están en esa ruta (no broadcast).
def push(route_id, evento):
payload = NOTIFICACIONES[evento]["pushPayload"] # de notificaciones.json
fcm.send_to_topic(f"topic_{route_id}", payload) # ej. "topic_RUTA-01"
# El ciudadano se suscribe SOLO al topic de su routeId → nunca recibe de otra ruta.
# El payload solo lleva texto (title/body); JAMÁS lat/lng.
ETA sin coordenadas (endpoint ciudadano)
# GET /eta?address_id=123 (ciudadano)
route = get_route_for_address(address_id) # derivado de colonia
pos = route.current_position_id
if pos < 4: mensaje = "El camión va en camino a tu sector"
elif pos == 4: mensaje = "Llega en aproximadamente 15 minutos"
elif pos < 8: mensaje = "Está atendiendo tu zona; saca tus bolsas"
else: mensaje = "Servicio del día finalizado"
# Devuelve SOLO texto:
return {"mensaje": mensaje, "status": route.status}
# NUNCA coordenadas.
RLS (el túnel a nivel de BD)
-- El ciudadano SOLO ve sus domicilios (refuerzo del túnel):
CREATE POLICY citizen_own_addresses ON addresses
FOR SELECT USING (auth.uid() = user_id);
-- route_positions (coordenadas): sin policy para citizen = sin acceso. Solo admin:
CREATE POLICY admin_route_positions ON route_positions
FOR SELECT USING (
(SELECT role FROM users WHERE id = auth.uid()) = 'admin'
);
7. Lógica de turnos y reasignación
De las notas del equipo:
- Una ruta es matutina O vespertina, nunca ambas.
- Ruta matutina que no se completó → se difiere al día siguiente + notifica.
- Ruta vespertina que la unidad no puede atender → la reemplaza una unidad de mañana (ya libre) + notifica. (Rancho Seco / RUTA-05 es el caso de demo.)
- Cada cambio escribe en
route_changesy dispara FCM con eventoreasignacion/retrasoal topic de la ruta.
8. Reparto del equipo (4 personas)
| Persona | Bloque | Responsabilidad |
|---|---|---|
| P1 (Líder) | Setup + Backend & Seguridad | Inicializar Flutter/backend, arquitectura API REST, JWT, RBAC/RLS, schema, validación de domicilio (OCR + privacidad). |
| P2 | Simulación + Notificaciones + Datos | Carga de los 3 JSON, simulación por positionId (cron), motor de eventos/FCM, lógica de turnos/reasignación, endpoint de ETA por texto. |
| P3 | Frontend Ciudadano (Flutter) | Registro, alta/validación de domicilios (dropdown colonia), vista ETA, suscripción FCM al topic de su ruta, buzón de retroalimentación, quiz de satisfacción. |
| P4 | Frontend Admin + Chofer + Mascota IA | Paneles admin (mapa de posiciones, única vista con coordenadas) y chofer (marcar avance, reportar falla), mascota interactiva (LLM), guía de separación offline (JSON local). |
Pair programming: VS Code Live Share. Parejas sugeridas: P1↔P2 (backend/datos) y P3↔P4 (Flutter).
9. Estructura de archivos del monorepo
ONLINESHACK/ <- raíz del repo (git aquí)
├── .gitignore <- raíz (cubre Flutter + Python)
├── README.md
├── PLAN.md <- el checklist del proyecto
├── claude.md <- este archivo
├── recolecta_app/ <- Flutter
│ ├── lib/
│ │ ├── core/ (theme, network/dio_client, auth, constants)
│ │ ├── features/ (auth, addresses, eta, notifications,
│ │ │ separation_guide, feedback, admin, driver)
│ │ └── shared/ (widgets reutilizables)
│ ├── assets/
│ │ └── .env <- (opcional) config pública: API_BASE_URL, anon key
│ ├── pubspec.yaml
│ └── ...
└── backend/ <- FastAPI
├── app/
│ ├── api/ (routers: auth, addresses, routes, eta, feedback, admin)
│ ├── core/ (config, security/jwt, deps)
│ ├── schemas/ (DTOs Pydantic)
│ ├── services/ (simulation, notifications, reassignment, ocr)
│ ├── data/ <- los 3 JSON provistos
│ │ ├── colonias-rutas.json
│ │ ├── notificaciones.json
│ │ └── rutas.json
│ └── db/ (session, seed.py)
├── secrets/
│ └── firebase-adminsdk.json <- ignorado
├── .env <- secretos reales (ignorado)
├── .env.example <- plantilla (versionada)
├── requirements.txt
└── main.py
10. Secretos y .env
Backend .env (secretos reales, nunca al cliente):
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=eyJ... # llave pública; puede ir al cliente
SUPABASE_SERVICE_ROLE_KEY=eyJ... # llave secreta; SOLO backend
JWT_SECRET=xxx
FIREBASE_CREDENTIALS_PATH=./secrets/firebase-adminsdk.json
SIMULATION_TICK_SECONDS=10
Flutter .env (SOLO config pública, viaja en el .apk/.ipa):
API_BASE_URL=http://10.0.2.2:8000 # 10.0.2.2 = localhost desde emulador Android
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=eyJ... # llave anon: pública por diseño
Principio: cualquier cosa que meta en Flutter es extraíble → no es secreto. Los secretos reales (service_role, admin SDK) viven únicamente en el backend.
11. Decisiones arquitectónicas clave (y por qué)
-
Supabase RLS en vez de RBAC solo en código: aunque alguien evada la API (bug, exploit), la BD niega el acceso. Es "defense in depth" para Privacidad por Diseño.
-
Simulación por
positionIden vez de cálculo geoespacial: los datos provistos ya definen 8 posiciones fijas con timestamps. Avanzar un índice es trivial, y los eventos salen denotificaciones.jsonsin cálculos. Evita complejidad innecesaria. -
FCM por topic de ruta en vez de push individual: escala mejor (un mensaje llega a todos los ciudadanos de la ruta). El túnel se resuelve en la suscripción: cada quien solo está en
topic_{su_routeId}. -
Dropdown de colonia en vez de geocodificación: los datos ya mapean colonia→ruta. Geocodificar sería redundante y costaría llamadas a API. El dropdown es más rápido, más barato y suficiente para el MVP.
-
OCR + borrado del recibo: cumple "Privacidad por Diseño" literalmente — la imagen nunca persiste. Solo queda
verified=true+ método + timestamp. -
Guía offline + chat IA: la guía estática (JSON embebido en Flutter) garantiza que funcione sin red. El chat con LLM es la capa extra cuando hay conexión.
12. Endpoints clave del backend
Ciudadano (autenticado, rol citizen)
POST /auth/register— registro con email/teléfonoPOST /auth/login— devuelve JWTGET /colonias— lista para el dropdown (nombre + horario)POST /addresses— dar de alta domicilio (calle + colonia elegida → derivaroute_id)POST /addresses/{id}/verify— sube recibo, OCR extrae dirección, compara, borra imagen, guardaverified=trueGET /eta?address_id=X— devuelve{mensaje, status}SIN coordenadasPOST /feedback— queja/rating hacia la unidad (NO chofer)
Chofer (rol driver)
GET /routes/mine— su ruta/unidad asignadaPOST /collections/{route_id}— marcar recolección en un domicilioPOST /incidents— reportar falla mecánica (dispara reasignación)
Admin (rol admin)
GET /routes— todas las rutas +current_position_idGET /routes/{id}/positions— las 8 coordenadas deroute_positions(⚠️ única vista con lat/lng)POST /routes/{id}/reassign— reasignar ruta a otra unidad + dispara FCMGET /drivers— lista de choferes + su unidad
13. Checklist de validación de privacidad (antes de cada demo)
Antes de mostrar cualquier feature, revisar:
- ¿El endpoint del ciudadano devuelve coordenadas? → ❌ rechazado
- ¿Hay un mapa con el camión moviéndose en vista ciudadana? → ❌ rechazado
- ¿El ciudadano puede pedir datos de otra ruta u otro usuario? → ❌ debe fallar (RLS)
- ¿Las quejas exponen el nombre/ID del chofer? → ❌ solo
target_unit_id - ¿El recibo del ciudadano quedó almacenado en BD o storage? → ❌ debe estar borrado
- ¿Los mensajes desalientan sacar basura fuera de horario? → ✅ validar textos
14. Comandos útiles de arranque
Flutter
cd recolecta_app
flutter pub get
flutter run # Android: conectar emulador o dispositivo
Backend
cd backend
python -m venv .venv && source .venv/bin/activate # (Windows: .venv\Scripts\activate)
pip install -r requirements.txt
# Crear .env copiando .env.example y llenando valores
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
Simulación (una vez el backend esté corriendo)
El cron de APScheduler arranca automáticamente con el backend si está configurado. Para forzar un tick manual desde consola Python:
from app.services.simulation import tick
tick() # avanza current_position_id de cada ruta y dispara eventos
15. Para la demo (guion de 5–7 min)
- Registro + validación de domicilio (mostrar que el recibo se borra).
- Correr la simulación acelerada (tick cada 5 segundos): llega
ROUTE_START→TRUCK_PROXIMITY→ROUTE_COMPLETED. Resaltar: sin mapa, sin ubicación, solo texto. - Mascota IA enseñando a separar residuos + guía offline.
- Falla de unidad en Rancho Seco (RUTA-05 vespertina) → reasignación a unidad matutina → notificación al ciudadano.
- Panel admin con el mapa de las 8 posiciones (contraste: solo admin ve coordenadas).
- Quiz post-recolección + queja a la unidad (sin exponer chofer).
- Diapositiva de arquitectura + cómo cada decisión cumple Privacidad por Diseño.
Fin del contexto. Este documento debe ir al repo como claude.md para que el equipo lo comparta con sus asistentes de IA.