Recorrido virtual inmersivo del Campus Tecnológico de Ternera de la Universidad Tecnológica de Bolívar. Incluye un mapa del campus, panorámicas 360°, narraciones en 11 idiomas con subtítulos y ajustes de accesibilidad (WCAG 2.2 AA).
El tour está diseñado como un módulo de la app oficial UTB APP, con su mismo lenguaje visual, y se puede incrustar en ella como un paquete Flutter. Mientras esa integración no ocurra, una app de demostración lo abre por su cuenta, con o sin servidor.
| Parte | Carpeta | Tecnología | Qué hace |
|---|---|---|---|
| Backend | services/api/ |
ASP.NET Core (.NET 10), EF Core, PostgreSQL y Storage de Supabase | API de contenido para la app y API de administración para el panel. |
| Frontend móvil | packages/tour_virtual/ |
Flutter, Riverpod, go_router | El módulo: todo el tour, listo para incrustarse en la UTB APP. |
apps/mobile/ |
Flutter | App de demostración que abre el módulo, genera el APK y corre las pruebas. | |
| Frontend admin | apps/admin/ |
Next.js 16, React 19 | Panel para editar lugares, escenas y medios. Hoy lista los POI. |
Además:
| Carpeta | Qué es |
|---|---|
packages/contracts/ |
Contrato OpenAPI de la API. |
scripts/ |
Plano del campus (OpenStreetMap), narraciones, carga de medios y APK de demostración. |
supabase/ |
Esquema y políticas RLS de la primera versión, que leía Supabase directamente. Es histórico: la API usa las migraciones de EF Core. |
docs/ |
Documentación (ver abajo). |
La UTB APP agrega el módulo como dependencia y lo abre como una pantalla más:
dependencies:
tour_virtual:
git:
url: https://github.com/ISCOUTB/Tour-Virtual-UTB
path: packages/tour_virtualNavigator.of(context).push(MaterialPageRoute<void>(
builder: (context) => TourVirtual(
configuracion: ConfiguracionTour(
fuente: const ContenidoRemoto(urlApi: 'https://api.tour.utb.edu.co'),
alSalir: () => Navigator.of(context).pop(),
),
),
));Ni la integración ni la decisión entre datos en la nube (ContenidoRemoto, la API de services/api) y datos locales (ContenidoLocal, sin servidor) dependen del equipo. Las dos vías están implementadas y se elige una con un solo parámetro. Requisitos, opciones y pendientes de la UTB: docs/integracion-utb-app.md.
cd apps/mobile
flutter pub get
flutter run --dart-define=DATOS=demo # sin servidor: contenido local, sin medios
flutter run # contra la API local (http://127.0.0.1:5209)
flutter testDesde un emulador Android la API local es http://10.0.2.2:5209; la app ya lo resuelve. Para un APK que funciona sin red y con medios: python scripts/preparar_apk_demo.py (necesita los medios institucionales, que no se versionan).
Para trabajar en el código del módulo (modelos, textos):
cd packages/tour_virtual
flutter pub get
dart run build_runner build # tras cambiar un modelo
flutter gen-l10n # tras cambiar un .arbRequiere el SDK de .NET 10 y acceso a la base del proyecto en Supabase. Las credenciales se guardan con dotnet user-secrets, nunca en el repositorio:
cd services/api
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=aws-1-us-west-2.pooler.supabase.com;Port=5432;Database=postgres;Username=postgres.qjnusmpwznaegeburldo;Password=[CONTRASEÑA_DB];SSL Mode=Require"
dotnet user-secrets set "SupabaseS3:ServiceUrl" "https://[PROYECTO].supabase.co/storage/v1/s3"
dotnet user-secrets set "SupabaseS3:AccessKey" "[ACCESS_KEY]"
dotnet user-secrets set "SupabaseS3:SecretKey" "[SECRET_KEY]"
dotnet user-secrets set "Admin:ApiKey" "[UNA_CLAVE_LARGA_Y_ALEATORIA]"
dotnet run --launch-profile http- Base de datos: usa el session pooler (puerto
5432). Con el transaction pooler (puerto6543) la API se queda colgada al conectar. La conexión directa (db.[PROYECTO].supabase.co) solo funciona con IPv6. La contraseña es la de la base (Supabase → Project Settings → Database), y el usuario lleva el id del proyecto pegado. - Clave de administración: los endpoints que escriben (
/api/admin/*) piden la cabeceraX-Admin-Key. Sin clave configurada, enDevelopmentquedan abiertos (la API lo avisa al arrancar) y en cualquier otro entorno responden503. - Login de administradores (Supabase Auth): el panel también puede entrar con el token de sesión de Supabase, en
Authorization: Bearer <access_token>. La API comprueba el token con las claves públicas deSupabase:Url(enappsettings.json) y deja pasar solo a quien tengarol = 'admin'en la tablaperfiles(supabase/migrations/0002_perfiles_y_auth.sql). Responde401sin token válido y403si la persona no es admin. Si el proyecto aún firma con el JWT secret antiguo, guárdalo condotnet user-secrets set "Supabase:JwtSecret" "...".X-Admin-Keysigue funcionando para Swagger y los scripts. - Uso: la API queda en
http://localhost:5209y Swagger en/swagger(botón Authorize para la clave).scripts/correr_api.ps1la levanta y reenvía el puerto a un teléfono conectado por USB. - Migraciones:
dotnet ef database update, solo contra la base prevista para tu entorno.
Todo se hace desde Swagger con la clave de administración.
- Videos, audios y fotos de un POI:
POST /api/admin/pois/{poiId}/medios. Sube el archivo y crea el medio en un solo paso.- Elige
Tipo(video,audiooimagen). Las imágenes pidenTextoAlt. - Para video usa MP4 (H.264 + AAC). El límite es 50 MB.
- Las fotos se reducen a 2048 px y pierden los metadatos EXIF.
- Elige
- Panorámicas:
- Crea la escena con
POST /api/admin/escenas, conImagenUrlvacío. - Sube la imagen con
POST /api/admin/escenas/{id}/imagen. El servidor la reduce a 4096×2048, genera la vista previa de 1024×512 y borra el EXIF.
- Crea la escena con
- Carpeta es opcional. Si se indica, debe ser
panoramas,mapas,medios,imagenes,videosoaudios. - URL externa: un medio también puede apuntar a una URL
https://...creándolo conPOST /api/admin/medios. Se entrega tal cual, sin firmar.
Las tareas pendientes del backend están en docs/tareas-backend.md (issue #10).
Con la API corriendo en http://localhost:5209, que el panel alcanza mediante el proxy de next.config.ts:
cd apps/admin
npm install
npm run dev # http://localhost:3000mastercontiene la última versión integrada de las tres partes.- Las ramas de trabajo salen de
mastery vuelven por pull request. Se nombran<parte>/<tema>, por ejemplomovil/realidad-aumentada,backend/medios-idiomaoadmin/formulario-poi. - Las ramas anteriores a la reorganización del 2026-10-29 están archivadas como etiquetas
archivo/<rama>. Se recuperan congit switch -c <rama> archivo/<rama>.
CI (GitHub Actions) verifica en cada pull request:
- el módulo: formato, código generado y análisis;
- la app de demostración: pruebas y APK de depuración;
- la API: compilación y dependencias vulnerables;
- el panel admin: dependencias vulnerables, análisis y compilación;
- el contrato OpenAPI.
| Documento | Para qué |
|---|---|
docs/GUIA_DESARROLLO.md |
Manual operativo: instalación, ejecución, contrato HTTP, pruebas y flujo Git. |
docs/integracion-utb-app.md |
Cómo se incrusta el módulo en la UTB APP y qué falta del lado de la UTB. |
docs/tareas-backend.md |
Lo que le falta al backend y al panel. |
docs/arquitectura.md |
Arquitectura, seguridad y cumplimiento de la Ley 1581 (sección 7). |
docs/ACCESIBILIDAD.md |
Idiomas, accesibilidad, formato de audios y subtítulos. |
docs/mapa-osm.md |
El plano del campus: origen, correcciones y precisión de cada lugar. |
docs/plan-realidad-aumentada.md |
Plan para la navegación con realidad aumentada del SRS. |
docs/utb-tour-inmersivo-mvp.md |
Alcance original del MVP. |
PRODUCT.md, DESIGN.md |
Producto y sistema visual. |
El SRS (SRSIEEE830TourVirtualUTBv3.1.pdf) es la autoridad sobre los requisitos. Como el resto del material institucional, no está en el repositorio: se pide al equipo.
- No se versionan credenciales,
.env, keystores ni cadenas de conexión reales. - Panorámicas, audios, el mapa ilustrado y los documentos fuente son material de la universidad. Viven fuera del repositorio (
assets/, ignorado). - Toda panorámica pasa por el difuminado de caras y la eliminación de EXIF antes de publicarse (Ley 1581; ver
docs/arquitectura.md, sección 7). - Las capturas de la UTB APP muestran datos personales y no se suben.
- El panel admin nunca envía
X-Admin-Keydesde el navegador. Cuando escriba contenido, debe hacerlo desde el servidor de Next.js (route handlers o server actions), con la clave en una variable de entorno sin el prefijoNEXT_PUBLIC_. Además, el panel necesita su propio inicio de sesión antes de publicarse. - La API solo acepta imágenes que puede volver a codificar (JPEG, PNG, WebP; nunca SVG) y de hasta 64 megapíxeles.
- El CI falla si una dependencia tiene una vulnerabilidad conocida:
npm auditen el panel ydotnet list package --vulnerableen la API. Las acciones de terceros van fijadas por commit, y los workflows solo tienen permiso de lectura.