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:
4
backend/.gitignore
vendored
Normal file
4
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
secrets/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.venv/
|
||||
46
backend/FIREBASE_SETUP.md
Normal file
46
backend/FIREBASE_SETUP.md
Normal 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
27
backend/README.md
Normal 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
1
backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Inicializa el paquete db
|
||||
45
backend/app/api/eta.py
Normal file
45
backend/app/api/eta.py
Normal 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}
|
||||
@@ -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"}
|
||||
]
|
||||
@@ -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."}
|
||||
}
|
||||
]
|
||||
@@ -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
107
backend/app/db/seed.py
Normal 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
18
backend/app/main.py
Normal 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)
|
||||
9
backend/app/services/__init__.py
Normal file
9
backend/app/services/__init__.py
Normal 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,
|
||||
)
|
||||
49
backend/app/services/notifications.py
Normal file
49
backend/app/services/notifications.py
Normal 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)}
|
||||
94
backend/app/services/simulation.py
Normal file
94
backend/app/services/simulation.py
Normal 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
36
backend/main.py
Normal 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
14
backend/requirements.txt
Normal 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
|
||||
Reference in New Issue
Block a user