Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>

Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com>
Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com>

configuracion inicial para supoabase y endpoints
This commit is contained in:
shinra32
2026-05-22 17:06:17 -06:00
parent f1ae9a301f
commit ba5e5ea12c
31 changed files with 4549 additions and 238 deletions

4
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
secrets/
*.pyc
__pycache__/
.venv/

46
backend/FIREBASE_SETUP.md Normal file
View File

@@ -0,0 +1,46 @@
# Firebase setup for Recolecta project
This document explains the manual steps to create a Firebase project, register Android/iOS apps, obtain the Admin SDK credentials for backend (FCM), and connect the Flutter app.
1) Create a Firebase project
- Go to https://console.firebase.google.com/ and create a new project (e.g., `recolecta-demo`).
2) Register Android app
- In the Firebase console, add an Android app. Use the app package name that matches your Flutter app (check `android/app/src/main/AndroidManifest.xml` or `android/app/build.gradle` `applicationId`).
- Download `google-services.json` and place it in the Flutter project at `recolecta_app/android/app/google-services.json`.
- Update `android/build.gradle` and `android/app/build.gradle` if needed (standard Flutter - Firebase steps). See https://firebase.flutter.dev/docs/overview/#installation
3) Register iOS app
- Add an iOS app in Firebase, set the iOS bundle id from your Flutter project (check `ios/Runner.xcodeproj`), download `GoogleService-Info.plist` and add it to `recolecta_app/ios/Runner/GoogleService-Info.plist` (open Xcode and add to Runner target).
4) Enable Cloud Messaging
- In Firebase console go to Cloud Messaging and ensure that your app is configured.
5) Obtain Admin SDK credentials (Backend)
- In Firebase Console: Project Settings → Service Accounts → Generate new private key.
- This downloads a JSON file like `recolecta-adminsdk-xxxxx.json`.
- Copy that file to the backend secrets folder: `backend/secrets/firebase-adminsdk.json` (create `backend/secrets/` if missing).
- IMPORTANT: do NOT commit this file to git. `backend/.gitignore` already excludes `secrets/`.
6) Set environment variable for backend
- Set `FIREBASE_CREDENTIALS_PATH` to the path where you placed the JSON, example in `.env`:
```
FIREBASE_CREDENTIALS_PATH=backend/secrets/firebase-adminsdk.json
```
7) Flutter configuration (pubspec)
- Add `firebase_core` and `firebase_messaging` to `recolecta_app/pubspec.yaml` and run `flutter pub get`.
- Initialize Firebase in Flutter `main()` per docs: `WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp();`
- Subscribe the citizen client to the route topic after confirming their `routeId`:
```dart
FirebaseMessaging.instance.subscribeToTopic('topic_RUTA-01');
```
8) Testing push (backend)
- The backend contains `app/services/notifications.py` which will use the Admin SDK if `FIREBASE_CREDENTIALS_PATH` is set and the file exists. Otherwise it falls back to a mock that prints messages.
- Start backend and trigger `POST /simulate/tick` to see mock pushes or real pushes if Admin SDK is configured.
9) Security note
- Keep the Admin SDK JSON secret. Use environment-managed secrets in production (Cloud Run secret manager, etc.).

27
backend/README.md Normal file
View File

@@ -0,0 +1,27 @@
# Backend (FastAPI) - Minimal scaffold
Este directorio contiene un scaffold mínimo para la API de simulación.
Requisitos
- Python 3.9+
- Crear un virtualenv e instalar dependencias:
```bash
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
```
Ejecutar la app
```bash
# desde la carpeta backend
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
Endpoints útiles
- `GET /colonias` — lista de colonias (mapea a `routeId`)
- `GET /eta?colonia=Zona%20Centro` — devuelve `mensaje` y `status` textual (sin coordenadas)
- `POST /simulate/tick` — avanza la simulación un paso y devuelve los eventos disparados

1
backend/app/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Inicializa el paquete db

45
backend/app/api/eta.py Normal file
View File

@@ -0,0 +1,45 @@
from fastapi import APIRouter, HTTPException
from typing import Optional
from app.services import simulation
router = APIRouter()
@router.get("/colonias")
def list_colonias():
return simulation.get_colonias()
@router.get("/eta")
def get_eta(colonia: Optional[str] = None, routeId: Optional[str] = None):
# Resolver routeId a partir de colonia si es necesario
if routeId is None:
if colonia is None:
raise HTTPException(status_code=400, detail="colonia or routeId required")
mapping = simulation.get_colonias()
match = next((c for c in mapping if c.get("colonia","").lower() == colonia.lower()), None)
if not match:
raise HTTPException(status_code=404, detail="colonia not found")
routeId = match["routeId"]
pos = simulation.get_route_position(routeId)
status = simulation.get_route_status(routeId)
if pos is None:
raise HTTPException(status_code=404, detail="route not found")
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"
return {"mensaje": mensaje, "status": status, "routeId": routeId}
@router.post("/simulate/tick")
def simulate_tick():
events = simulation.tick()
return {"events": events}

View File

@@ -1,9 +1,9 @@
[
{ "colonia": "Zona Centro", "routeId": "RUTA-01", "horarioEstimado": "Matutino (06:30 - 07:15)" },
{ "colonia": "Las Arboledas", "routeId": "RUTA-01", "horarioEstimado": "Matutino (07:00 - 07:30)" },
{ "colonia": "Trojes", "routeId": "RUTA-13", "horarioEstimado": "Matutino (06:40 - 07:10)" },
{ "colonia": "San Juanico", "routeId": "RUTA-03", "horarioEstimado": "Matutino (06:45 - 07:15)" },
{ "colonia": "Los Olivos", "routeId": "RUTA-04", "horarioEstimado": "Matutino (07:00 - 07:40)" },
{ "colonia": "Rancho Seco", "routeId": "RUTA-05", "horarioEstimado": "Vespertino (14:15 - 15:00)" },
{ "colonia": "Las Insurgentes", "routeId": "RUTA-12", "horarioEstimado": "Matutino (06:35 - 07:10)" }
{"colonia": "Zona Centro", "routeId": "RUTA-01", "turno": "Matutino", "horario_estimado": "07:00 - 09:00"},
{"colonia": "Las Arboledas", "routeId": "RUTA-01", "turno": "Matutino", "horario_estimado": "08:30 - 10:30"},
{"colonia": "San Juanico", "routeId": "RUTA-03", "turno": "Matutino", "horario_estimado": "07:00 - 09:00"},
{"colonia": "Los Olivos", "routeId": "RUTA-04", "turno": "Matutino", "horario_estimado": "09:00 - 11:00"},
{"colonia": "Rancho Seco", "routeId": "RUTA-05", "turno": "Vespertino", "horario_estimado": "18:00 - 20:00"},
{"colonia": "Las Insurgentes", "routeId": "RUTA-12", "turno": "Matutino", "horario_estimado": "10:00 - 12:00"},
{"colonia": "Trojes", "routeId": "RUTA-13", "turno": "Matutino", "horario_estimado": "11:00 - 13:00"}
]

View File

@@ -1,26 +1,14 @@
[
{
"triggerEvent": "ROUTE_START",
"condition": "Cuando positionId cambia de 1 a 2",
"pushPayload": {
"title": "¡Ruta Iniciada!",
"body": "El camión recolector ha salido del Relleno Sanitario rumbo a tu sector. Asegúrate de tener listos tus residuos."
}
"pushPayload": {"title": "Ruta Iniciada", "body": "El camión salió del relleno rumbo a tu sector."}
},
{
"triggerEvent": "TRUCK_PROXIMITY",
"condition": "Cuando positionId llega a 4 (punto previo al destino)",
"pushPayload": {
"title": "Camión Cercano",
"body": "El camión está a menos de 15 minutos de tu domicilio. Es momento de sacar tus bolsas a la acera."
}
"pushPayload": {"title": "Camión Cerca", "body": "A menos de 15 min; saca tus bolsas."}
},
{
"triggerEvent": "ROUTE_COMPLETED",
"condition": "Cuando positionId llega a 8 (retorno al basurero)",
"pushPayload": {
"title": "Servicio Finalizado",
"body": "El camión de tu sector ha concluido su jornada de recolección diaria."
}
"pushPayload": {"title": "Servicio Finalizado", "body": "El servicio del día en tu zona ha concluido."}
}
]

View File

@@ -1,242 +1,57 @@
[
{
"routeId": "RUTA-01",
"name": "Zona Centro - Las Arboledas",
"truckId": 101,
"status": "EN_RUTA",
"turno": "matutino",
"status": "PENDIENTE",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:00:00Z" },
{ "positionId": 2, "lat": 20.5185, "lng": -100.8450, "speed": 45, "timestamp": "2026-05-22T06:12:00Z" },
{ "positionId": 3, "lat": 20.5215, "lng": -100.8142, "speed": 22, "timestamp": "2026-05-22T06:25:00Z" },
{ "positionId": 4, "lat": 20.5212, "lng": -100.8175, "speed": 15, "timestamp": "2026-05-22T06:38:00Z" },
{ "positionId": 5, "lat": 20.5210, "lng": -100.8210, "speed": 0, "timestamp": "2026-05-22T06:50:00Z" },
{ "positionId": 6, "lat": 20.5235, "lng": -100.8212, "speed": 18, "timestamp": "2026-05-22T07:05:00Z" },
{ "positionId": 7, "lat": 20.5260, "lng": -100.8215, "speed": 20, "timestamp": "2026-05-22T07:18:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 40, "timestamp": "2026-05-22T07:40:00Z" }
]
},
{
"routeId": "RUTA-02",
"name": "Sector Norte - Av. Tecnológico",
"truckId": 102,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:05:00Z" },
{ "positionId": 2, "lat": 20.5280, "lng": -100.8135, "speed": 38, "timestamp": "2026-05-22T06:18:00Z" },
{ "positionId": 3, "lat": 20.5410, "lng": -100.8130, "speed": 25, "timestamp": "2026-05-22T06:30:00Z" },
{ "positionId": 4, "lat": 20.5445, "lng": -100.8132, "speed": 12, "timestamp": "2026-05-22T06:45:00Z" },
{ "positionId": 5, "lat": 20.5480, "lng": -100.8135, "speed": 0, "timestamp": "2026-05-22T06:58:00Z" },
{ "positionId": 6, "lat": 20.5515, "lng": -100.8138, "speed": 15, "timestamp": "2026-05-22T07:10:00Z" },
{ "positionId": 7, "lat": 20.5540, "lng": -100.8110, "speed": 22, "timestamp": "2026-05-22T07:25:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 45, "timestamp": "2026-05-22T07:50:00Z" }
]
},
{
"routeId": "RUTA-03",
"name": "Sector Poniente - San Juanico",
"truckId": 103,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:10:00Z" },
{ "positionId": 2, "lat": 20.5250, "lng": -100.8510, "speed": 42, "timestamp": "2026-05-22T06:20:00Z" },
{ "positionId": 3, "lat": 20.5290, "lng": -100.8320, "speed": 20, "timestamp": "2026-05-22T06:35:00Z" },
{ "positionId": 4, "lat": 20.5315, "lng": -100.8355, "speed": 15, "timestamp": "2026-05-22T06:48:00Z" },
{ "positionId": 5, "lat": 20.5340, "lng": -100.8390, "speed": 0, "timestamp": "2026-05-22T07:00:00Z" },
{ "positionId": 6, "lat": 20.5362, "lng": -100.8425, "speed": 10, "timestamp": "2026-05-22T07:15:00Z" },
{ "positionId": 7, "lat": 20.5330, "lng": -100.8430, "speed": 18, "timestamp": "2026-05-22T07:28:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 35, "timestamp": "2026-05-22T07:45:00Z" }
]
},
{
"routeId": "RUTA-04",
"name": "Oriente - Los Olivos",
"truckId": 104,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:15:00Z" },
{ "positionId": 2, "lat": 20.5260, "lng": -100.8010, "speed": 45, "timestamp": "2026-05-22T06:30:00Z" },
{ "positionId": 3, "lat": 20.5295, "lng": -100.7890, "speed": 24, "timestamp": "2026-05-22T06:45:00Z" },
{ "positionId": 4, "lat": 20.5320, "lng": -100.7850, "speed": 12, "timestamp": "2026-05-22T06:58:00Z" },
{ "positionId": 5, "lat": 20.5350, "lng": -100.7790, "speed": 0, "timestamp": "2026-05-22T07:12:00Z" },
{ "positionId": 6, "lat": 20.5310, "lng": -100.7760, "speed": 15, "timestamp": "2026-05-22T07:25:00Z" },
{ "positionId": 7, "lat": 20.5270, "lng": -100.7820, "speed": 26, "timestamp": "2026-05-22T07:38:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 48, "timestamp": "2026-05-22T07:58:00Z" }
{"positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 40, "ts": "2026-05-01T08:00:00Z"},
{"positionId": 2, "lat": 20.5150, "lng": -100.9000, "speed": 35, "ts": "2026-05-01T08:10:00Z"},
{"positionId": 3, "lat": 20.5200, "lng": -100.8950, "speed": 20, "ts": "2026-05-01T08:20:00Z"},
{"positionId": 4, "lat": 20.5250, "lng": -100.8900, "speed": 15, "ts": "2026-05-01T08:30:00Z"},
{"positionId": 5, "lat": 20.5300, "lng": -100.8850, "speed": 0, "ts": "2026-05-01T08:40:00Z"},
{"positionId": 6, "lat": 20.5250, "lng": -100.8900, "speed": 25, "ts": "2026-05-01T08:50:00Z"},
{"positionId": 7, "lat": 20.5150, "lng": -100.9000, "speed": 30, "ts": "2026-05-01T09:00:00Z"},
{"positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 0, "ts": "2026-05-01T09:10:00Z"}
]
},
{
"routeId": "RUTA-05",
"name": "Sector Sur - Rancho Seco",
"truckId": 105,
"status": "EN_RUTA",
"turno": "vespertino",
"status": "PENDIENTE",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:20:00Z" },
{ "positionId": 2, "lat": 20.5050, "lng": -100.8620, "speed": 35, "timestamp": "2026-05-22T06:32:00Z" },
{ "positionId": 3, "lat": 20.5020, "lng": -100.8350, "speed": 22, "timestamp": "2026-05-22T06:45:00Z" },
{ "positionId": 4, "lat": 20.4995, "lng": -100.8210, "speed": 14, "timestamp": "2026-05-22T06:58:00Z" },
{ "positionId": 5, "lat": 20.4970, "lng": -100.8150, "speed": 0, "timestamp": "2026-05-22T07:10:00Z" },
{ "positionId": 6, "lat": 20.5010, "lng": -100.8120, "speed": 16, "timestamp": "2026-05-22T07:22:00Z" },
{ "positionId": 7, "lat": 20.5060, "lng": -100.8160, "speed": 25, "timestamp": "2026-05-22T07:35:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 40, "timestamp": "2026-05-22T07:55:00Z" }
{"positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 40, "ts": "2026-05-01T15:00:00Z"},
{"positionId": 4, "lat": 20.5250, "lng": -100.8900, "speed": 15, "ts": "2026-05-01T15:30:00Z"},
{"positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 0, "ts": "2026-05-01T16:10:00Z"}
]
},
{
"routeId": "RUTA-06",
"name": "Norte Extremo - Rumbos de Roque",
"truckId": 106,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:00:00Z" },
{ "positionId": 2, "lat": 20.5380, "lng": -100.8380, "speed": 40, "timestamp": "2026-05-22T06:15:00Z" },
{ "positionId": 3, "lat": 20.5610, "lng": -100.8370, "speed": 30, "timestamp": "2026-05-22T06:30:00Z" },
{ "positionId": 4, "lat": 20.5750, "lng": -100.8360, "speed": 15, "timestamp": "2026-05-22T06:45:00Z" },
{ "positionId": 5, "lat": 20.5820, "lng": -100.8350, "speed": 0, "timestamp": "2026-05-22T07:00:00Z" },
{ "positionId": 6, "lat": 20.5780, "lng": -100.8310, "speed": 20, "timestamp": "2026-05-22T07:15:00Z" },
{ "positionId": 7, "lat": 20.5650, "lng": -100.8320, "speed": 28, "timestamp": "2026-05-22T07:30:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 45, "timestamp": "2026-05-22T07:55:00Z" }
]
"routeId": "RUTA-03",
"truckId": 103,
"turno": "matutino",
"status": "PENDIENTE",
"positions": []
},
{
"routeId": "RUTA-07",
"name": "Nororiente - Ciudad Industrial",
"truckId": 107,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:10:00Z" },
{ "positionId": 2, "lat": 20.5350, "lng": -100.8050, "speed": 44, "timestamp": "2026-05-22T06:24:00Z" },
{ "positionId": 3, "lat": 20.5450, "lng": -100.7950, "speed": 25, "timestamp": "2026-05-22T06:38:00Z" },
{ "positionId": 4, "lat": 20.5480, "lng": -100.7850, "speed": 18, "timestamp": "2026-05-22T06:52:00Z" },
{ "positionId": 5, "lat": 20.5510, "lng": -100.7750, "speed": 0, "timestamp": "2026-05-22T07:05:00Z" },
{ "positionId": 6, "lat": 20.5460, "lng": -100.7720, "speed": 12, "timestamp": "2026-05-22T07:18:00Z" },
{ "positionId": 7, "lat": 20.5390, "lng": -100.7820, "speed": 30, "timestamp": "2026-05-22T07:30:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 42, "timestamp": "2026-05-22T07:52:00Z" }
]
},
{
"routeId": "RUTA-08",
"name": "Suroriente - Universidad Latina",
"truckId": 108,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:15:00Z" },
{ "positionId": 2, "lat": 20.5180, "lng": -100.8310, "speed": 38, "timestamp": "2026-05-22T06:28:00Z" },
{ "positionId": 3, "lat": 20.5245, "lng": -100.7980, "speed": 30, "timestamp": "2026-05-22T06:42:00Z" },
{ "positionId": 4, "lat": 20.5210, "lng": -100.7995, "speed": 14, "timestamp": "2026-05-22T06:55:00Z" },
{ "positionId": 5, "lat": 20.5175, "lng": -100.8010, "speed": 0, "timestamp": "2026-05-22T07:08:00Z" },
{ "positionId": 6, "lat": 20.5140, "lng": -100.8030, "speed": 18, "timestamp": "2026-05-22T07:20:00Z" },
{ "positionId": 7, "lat": 20.5110, "lng": -100.8055, "speed": 22, "timestamp": "2026-05-22T07:32:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 40, "timestamp": "2026-05-22T07:54:00Z" }
]
},
{
"routeId": "RUTA-09",
"name": "Poniente - Hospital General",
"truckId": 109,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:02:00Z" },
{ "positionId": 2, "lat": 20.5210, "lng": -100.8650, "speed": 45, "timestamp": "2026-05-22T06:12:00Z" },
{ "positionId": 3, "lat": 20.5260, "lng": -100.8520, "speed": 26, "timestamp": "2026-05-22T06:24:00Z" },
{ "positionId": 4, "lat": 20.5275, "lng": -100.8490, "speed": 12, "timestamp": "2026-05-22T06:36:00Z" },
{ "positionId": 5, "lat": 20.5285, "lng": -100.8460, "speed": 0, "timestamp": "2026-05-22T06:48:00Z" },
{ "positionId": 6, "lat": 20.5250, "lng": -100.8470, "speed": 15, "timestamp": "2026-05-22T07:00:00Z" },
{ "positionId": 7, "lat": 20.5220, "lng": -100.8550, "speed": 32, "timestamp": "2026-05-22T07:12:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 44, "timestamp": "2026-05-22T07:30:00Z" }
]
},
{
"routeId": "RUTA-10",
"name": "Eje Juan Pablo II - Sede UG Sur",
"truckId": 110,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:22:00Z" },
{ "positionId": 2, "lat": 20.5015, "lng": -100.8520, "speed": 40, "timestamp": "2026-05-22T06:34:00Z" },
{ "positionId": 3, "lat": 20.4990, "lng": -100.8390, "speed": 28, "timestamp": "2026-05-22T06:46:00Z" },
{ "positionId": 4, "lat": 20.4950, "lng": -100.8320, "speed": 18, "timestamp": "2026-05-22T06:58:00Z" },
{ "positionId": 5, "lat": 20.4920, "lng": -100.8280, "speed": 0, "timestamp": "2026-05-22T07:10:00Z" },
{ "positionId": 6, "lat": 20.4945, "lng": -100.8240, "speed": 14, "timestamp": "2026-05-22T07:22:00Z" },
{ "positionId": 7, "lat": 20.4980, "lng": -100.8300, "speed": 30, "timestamp": "2026-05-22T07:34:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 38, "timestamp": "2026-05-22T07:52:00Z" }
]
},
{
"routeId": "RUTA-11",
"name": "Zona de Oro - Torres Landa",
"truckId": 111,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:04:00Z" },
{ "positionId": 2, "lat": 20.5240, "lng": -100.8350, "speed": 36, "timestamp": "2026-05-22T06:16:00Z" },
{ "positionId": 3, "lat": 20.5280, "lng": -100.8250, "speed": 22, "timestamp": "2026-05-22T06:29:00Z" },
{ "positionId": 4, "lat": 20.5295, "lng": -100.8210, "speed": 10, "timestamp": "2026-05-22T06:42:00Z" },
{ "positionId": 5, "lat": 20.5310, "lng": -100.8170, "speed": 0, "timestamp": "2026-05-22T06:55:00Z" },
{ "positionId": 6, "lat": 20.5290, "lng": -100.8140, "speed": 16, "timestamp": "2026-05-22T07:08:00Z" },
{ "positionId": 7, "lat": 20.5260, "lng": -100.8220, "speed": 28, "timestamp": "2026-05-22T07:21:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 42, "timestamp": "2026-05-22T07:42:00Z" }
]
"routeId": "RUTA-04",
"truckId": 104,
"turno": "matutino",
"status": "PENDIENTE",
"positions": []
},
{
"routeId": "RUTA-12",
"name": "Nororiente - Las Insurgentes",
"truckId": 112,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:08:00Z" },
{ "positionId": 2, "lat": 20.5280, "lng": -100.8080, "speed": 40, "timestamp": "2026-05-22T06:22:00Z" },
{ "positionId": 3, "lat": 20.5320, "lng": -100.7980, "speed": 24, "timestamp": "2026-05-22T06:35:00Z" },
{ "positionId": 4, "lat": 20.5340, "lng": -100.7940, "speed": 15, "timestamp": "2026-05-22T06:48:00Z" },
{ "positionId": 5, "lat": 20.5360, "lng": -100.7900, "speed": 0, "timestamp": "2026-05-22T07:00:00Z" },
{ "positionId": 6, "lat": 20.5310, "lng": -100.7920, "speed": 12, "timestamp": "2026-05-22T07:12:00Z" },
{ "positionId": 7, "lat": 20.5270, "lng": -100.8020, "speed": 26, "timestamp": "2026-05-22T07:25:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 44, "timestamp": "2026-05-22T07:48:00Z" }
]
"turno": "matutino",
"status": "PENDIENTE",
"positions": []
},
{
"routeId": "RUTA-13",
"name": "Sector Norte - Trojes e Irrigación",
"truckId": 113,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:12:00Z" },
{ "positionId": 2, "lat": 20.5360, "lng": -100.8190, "speed": 35, "timestamp": "2026-05-22T06:26:00Z" },
{ "positionId": 3, "lat": 20.5420, "lng": -100.8080, "speed": 28, "timestamp": "2026-05-22T06:40:00Z" },
{ "positionId": 4, "lat": 20.5440, "lng": -100.8040, "speed": 14, "timestamp": "2026-05-22T06:54:00Z" },
{ "positionId": 5, "lat": 20.5460, "lng": -100.8000, "speed": 0, "timestamp": "2026-05-22T07:06:00Z" },
{ "positionId": 6, "lat": 20.5410, "lng": -100.8020, "speed": 18, "timestamp": "2026-05-22T07:18:00Z" },
{ "positionId": 7, "lat": 20.5370, "lng": -100.8120, "speed": 25, "timestamp": "2026-05-22T07:30:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 39, "timestamp": "2026-05-22T07:54:00Z" }
]
},
{
"routeId": "RUTA-14",
"name": "Sur Poniente - La Toscana",
"truckId": 114,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:16:00Z" },
{ "positionId": 2, "lat": 20.5150, "lng": -100.8580, "speed": 42, "timestamp": "2026-05-22T06:28:00Z" },
{ "positionId": 3, "lat": 20.5140, "lng": -100.8390, "speed": 26, "timestamp": "2026-05-22T06:41:00Z" },
{ "positionId": 4, "lat": 20.5125, "lng": -100.8310, "speed": 16, "timestamp": "2026-05-22T06:54:00Z" },
{ "positionId": 5, "lat": 20.5110, "lng": -100.8250, "speed": 0, "timestamp": "2026-05-22T07:06:00Z" },
{ "positionId": 6, "lat": 20.5135, "lng": -100.8280, "speed": 12, "timestamp": "2026-05-22T07:18:00Z" },
{ "positionId": 7, "lat": 20.5160, "lng": -100.8420, "speed": 32, "timestamp": "2026-05-22T07:30:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 45, "timestamp": "2026-05-22T07:51:00Z" }
]
},
{
"routeId": "RUTA-15",
"name": "Norponiente - Camino a San José de Celaya",
"truckId": 115,
"status": "EN_RUTA",
"positions": [
{ "positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "timestamp": "2026-05-22T06:18:00Z" },
{ "positionId": 2, "lat": 20.5320, "lng": -100.8590, "speed": 38, "timestamp": "2026-05-22T06:31:00Z" },
{ "positionId": 3, "lat": 20.5390, "lng": -100.8480, "speed": 24, "timestamp": "2026-05-22T06:44:00Z" },
{ "positionId": 4, "lat": 20.5420, "lng": -100.8440, "speed": 15, "timestamp": "2026-05-22T06:57:00Z" },
{ "positionId": 5, "lat": 20.5450, "lng": -100.8410, "speed": 0, "timestamp": "2026-05-22T07:09:00Z" },
{ "positionId": 6, "lat": 20.5410, "lng": -100.8430, "speed": 14, "timestamp": "2026-05-22T07:21:00Z" },
{ "positionId": 7, "lat": 20.5360, "lng": -100.8520, "speed": 28, "timestamp": "2026-05-22T07:33:00Z" },
{ "positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 41, "timestamp": "2026-05-22T07:54:00Z" }
]
"turno": "matutino",
"status": "PENDIENTE",
"positions": []
}
]

107
backend/app/db/seed.py Normal file
View File

@@ -0,0 +1,107 @@
import json
import os
from supabase import create_client, Client
# Configuración de directorios base
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(BASE_DIR, "data")
ENV_PATH = os.path.join(os.path.dirname(BASE_DIR), ".env")
def load_env(path: str):
"""Carga variables de entorno de forma manual sin depender de python-dotenv"""
if not os.path.exists(path):
print(f"Advertencia: No se encontró el archivo {path}")
return
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
# Separa por el primer '=' y limpia espacios y comillas
key, val = line.split('=', 1)
os.environ[key.strip()] = val.strip().strip("'").strip('"')
def load_json(filename: str):
path = os.path.join(DATA_DIR, filename)
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def main():
print("Iniciando proceso de seeding...")
load_env(ENV_PATH)
# Es crucial usar SUPABASE_SERVICE_ROLE_KEY para saltar el RLS durante el Seed
URL = os.environ.get("SUPABASE_URL")
KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
if not URL or not KEY:
raise ValueError("Error: Asegúrate de tener SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY en el .env")
supabase: Client = create_client(URL, KEY)
rutas_data = load_json("rutas.json")
colonias_data = load_json("colonias-rutas.json")
# 1. Poblar UNITS (Dependencia raíz)
print("1. Poblando tabla 'units'...")
units_to_insert = []
truck_ids = set()
for r in rutas_data:
tid = r.get("truckId")
if tid and tid not in truck_ids:
truck_ids.add(tid)
units_to_insert.append({
"id": tid,
"plate": f"GTO-{tid}", # Simulado
"status": "active"
})
if units_to_insert:
supabase.table("units").upsert(units_to_insert).execute()
# 2. Poblar ROUTES (Depende de units)
print("2. Poblando tabla 'routes'...")
routes_to_insert = []
for r in rutas_data:
routes_to_insert.append({
"id": r["routeId"],
"name": f"Ruta {r['routeId'].split('-')[1]}",
"truck_id": r["truckId"],
"turno": r.get("turno", "matutino"),
"status": r.get("status", "pendiente"),
"current_position_id": 1
})
if routes_to_insert:
supabase.table("routes").upsert(routes_to_insert).execute()
# 3. Poblar ROUTE_POSITIONS (Depende de routes)
print("3. Poblando tabla 'route_positions'...")
positions_to_insert = []
for r in rutas_data:
for pos in r.get("positions", []):
positions_to_insert.append({
"route_id": r["routeId"],
"position_id": pos["positionId"],
"lat": pos["lat"],
"lng": pos["lng"],
"speed": pos.get("speed", 0),
"ts": pos.get("ts")
})
if positions_to_insert:
supabase.table("route_positions").upsert(positions_to_insert).execute()
# 4. Poblar COLONIAS (Depende de routes)
print("4. Poblando tabla 'colonias'...")
if colonias_data:
colonias_to_insert = []
for c in colonias_data:
colonias_to_insert.append({
"nombre": c["colonia"],
"route_id": c["routeId"],
"turno": c["turno"],
"horario_estimado": c["horario_estimado"]
})
supabase.table("colonias").upsert(colonias_to_insert).execute()
print("✅ Seed completado con éxito. Base de datos operativa para la app.")
if __name__ == "__main__":
main()

18
backend/app/main.py Normal file
View File

@@ -0,0 +1,18 @@
from fastapi import FastAPI
from app.api.eta import router as eta_router
from app.services import simulation
from app.services import notifications
import os
app = FastAPI(title="Recoleccion API")
app.include_router(eta_router)
@app.on_event("startup")
async def startup_event():
# Carga los datos en memoria al iniciar la app
simulation.load_data()
simulation.start_simulation_state()
# Inicializar Firebase Admin si hay credenciales
cred_path = os.environ.get("FIREBASE_CREDENTIALS_PATH", "backend/secrets/firebase-adminsdk.json")
notifications.init_firebase(cred_path)

View File

@@ -0,0 +1,9 @@
from .simulation import (
load_data,
start_simulation_state,
get_colonias,
get_route_position,
get_route_status,
tick,
get_last_events,
)

View File

@@ -0,0 +1,49 @@
import os
from typing import Dict
_initialized = False
_use_mock = True
def init_firebase(credentials_path: str = None):
global _initialized, _use_mock
try:
import firebase_admin
from firebase_admin import credentials, messaging
except Exception:
_use_mock = True
return
if credentials_path is None:
credentials_path = os.environ.get('FIREBASE_CREDENTIALS_PATH', 'backend/secrets/firebase-adminsdk.json')
if not os.path.exists(credentials_path):
_use_mock = True
return
try:
cred = credentials.Certificate(credentials_path)
firebase_admin.initialize_app(cred)
_initialized = True
_use_mock = False
except Exception:
_use_mock = True
def send_to_topic(topic: str, payload: Dict):
"""Sends a push to an FCM topic. Falls back to mock (prints) if not configured."""
global _use_mock
if _use_mock:
print(f"[MOCK PUSH] topic={topic} payload={payload}")
return {"mock": True, "topic": topic, "payload": payload}
try:
from firebase_admin import messaging
message = messaging.Message(
notification=messaging.Notification(title=payload.get('title'), body=payload.get('body')),
topic=topic,
)
resp = messaging.send(message)
return {"result": resp}
except Exception as e:
print(f"[PUSH ERROR] {e}")
return {"error": str(e)}

View File

@@ -0,0 +1,94 @@
import json
import os
from typing import Dict, List, Optional
from app.services import notifications
ROOT = os.path.dirname(os.path.dirname(__file__)) # backend/app
DATA_DIR = os.path.join(ROOT, "data")
ROUTES: List[Dict] = []
NOTIFS: List[Dict] = []
COLONIAS: List[Dict] = []
ESTADO: Dict[str, int] = {}
STATUS: Dict[str, str] = {}
LAST_EVENTS: List[Dict] = []
def _load_json(filename: str):
path = os.path.join(DATA_DIR, filename)
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def load_data():
global ROUTES, NOTIFS, COLONIAS
ROUTES = _load_json("rutas.json")
NOTIFS = _load_json("notificaciones.json")
COLONIAS = _load_json("colonias-rutas.json")
def start_simulation_state():
"""Inicializa el estado (positionId) para cada ruta presente en `rutas.json`."""
global ESTADO, STATUS
ESTADO = {}
STATUS = {}
for r in ROUTES:
rid = r.get("routeId")
ESTADO[rid] = 1
STATUS[rid] = r.get("status", "PENDIENTE")
def get_colonias():
return COLONIAS
def get_route_position(routeId: str) -> Optional[int]:
return ESTADO.get(routeId)
def get_route_status(routeId: str) -> Optional[str]:
return STATUS.get(routeId)
def _find_notif(event_name: str) -> Optional[Dict]:
for n in NOTIFS:
if n.get("triggerEvent") == event_name:
return n
return None
def tick() -> List[Dict]:
"""Avanza todas las rutas en memoria (pos 1..8) y devuelve eventos disparados."""
global ESTADO, LAST_EVENTS
events = []
for route_id, pos in list(ESTADO.items()):
if pos < 8:
antes = pos
ahora = pos + 1
ESTADO[route_id] = ahora
evt = None
if antes == 1 and ahora == 2:
evt = "ROUTE_START"
elif ahora == 4:
evt = "TRUCK_PROXIMITY"
elif ahora == 8:
evt = "ROUTE_COMPLETED"
if evt:
notif = _find_notif(evt)
payload = notif.get("pushPayload") if notif else {"title": evt, "body": ""}
simulated = {"routeId": route_id, "event": evt, "payload": payload}
events.append(simulated)
LAST_EVENTS.append(simulated)
# Enviar push vía servicio de notificaciones (FCM) o mock
topic = f"topic_{route_id}"
try:
notifications.send_to_topic(topic, payload)
except Exception:
print(f"[SIM PUSH FAIL] {route_id} -> {evt}: {payload.get('title')} - {payload.get('body')}")
return events
def get_last_events() -> List[Dict]:
return LAST_EVENTS[-20:]

36
backend/main.py Normal file
View File

@@ -0,0 +1,36 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
# Aquí se importarán los routers en el futuro
# from app.api.routers import auth, addresses, routes, eta
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Maneja el ciclo de vida de la aplicación.
Ideal para arrancar el cron job de simulación (APScheduler).
"""
print("Iniciando aplicación: Backend Sistema de Recolección...")
# TODO: Inicializar APScheduler aquí para avanzar current_position_id (1-8)
yield
print("Apagando aplicación y deteniendo simulador...")
# TODO: Apagar APScheduler
app = FastAPI(
title="API - Recolección Inteligente y Privada",
description="Backend para el sistema de recolección de residuos con privacidad por diseño.",
version="1.0.0",
lifespan=lifespan
)
# Endpoints de prueba base
@app.get("/")
def read_root():
return {
"status": "ok",
"message": "Backend operativo. Regla Innegociable 1: NUNCA se devuelven coordenadas del camión al ciudadano."
}
@app.get("/health")
def health_check():
return {"status": "healthy"}

14
backend/requirements.txt Normal file
View File

@@ -0,0 +1,14 @@
fastapi>=0.95.0
uvicorn[standard]>=0.22.0
firebase-admin>=6.0.0
apscheduler>=3.10.1
fastapi==0.111.0
uvicorn[standard]==0.29.0
sqlalchemy==2.0.30
psycopg2-binary==2.9.9
apscheduler==3.10.4
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
pydantic-settings==2.2.1
supabase==2.4.5
firebase-admin==6.5.0