RADIX es un sistema educativo offline que simula un servidor de borde (Raspberry Pi) ejecutándose en la Amazonía. Combina un backend RESTful en Go con una SPA en React para ofrecer una experiencia LMS completa sin conexión a internet.
Los archivos se suben localmente al servidor de borde, los estudiantes consumen contenido offline ganando XP, y una cola DTN (Delay-Tolerant Networking) registra las transacciones para sincronización oportunista con un servidor central cuando la conectividad lo permita.
- 3 roles con RBAC: Profesor (Admin), Estudiante (Student), Invitado (Guest)
- LMS Offline: Cursos con inscripción de estudiantes (el contenido solo es visible para inscritos), lecciones en markdown, quizzes por curso y XP
- Foro por curso: Hilos y respuestas anidadas, likes, visible desde el curso
- Biblioteca Multimedia: Upload real de archivos (video, audio, imagen, PDF, texto, documento) con preview inline
- Videojuegos HTML: Un HTML subido puede marcarse como juego (
game=true); se sirve en un<iframe sandbox>con su propio reproductor (GamePlayer) - Wiki Syntax
[[id]]: Enlaza archivos, lecciones o quizzes dentro del contenido de una lección; se renderizan como media/tarjeta embebida - Editor de Lecciones: CodeMirror 6 con syntax highlight para
[[id]], hover tooltip con preview, y sidebar de archivos enlazados - Monitor del Servidor: Métricas en tiempo real (disco, usuarios activos, cola DTN)
- Observabilidad: Página de Logs dedicada — tail en vivo, historial filtrable (nivel/fecha/texto libre vía full-text search) y estadísticas, con retención configurable
- Auto-detección de metadatos: ffprobe extrae duración y resolución automáticamente al subir archivos
- Sincronización entre nodos: Cola DTN real — cada escritura se anota como una operación reproducible, los nodos pares tiran solo lo que les falta, los conflictos se resuelven por versión de fila (HLC) y los archivos se descargan aparte
- Red de pruebas en Docker: Varios servidores de borde completos hablando entre sí en una máquina (
./testnet.sh) - Autenticación real: Login por email/contraseña (bcrypt) + acceso invitado sin credenciales
- XP: puntos por lección completada y por quiz aprobado. (Las "medallas" del dashboard de estudiante son umbrales fijos calculados en el cliente, no un sistema de logros del backend — ver Roadmap.)
| Capa | Tecnología |
|---|---|
| Backend | Go 1.25, Echo v5, godotenv |
| Frontend | React 19, TypeScript, Vite 6 |
| Routing | React Router v6 |
| Estilos | Tailwind CSS v3 |
| Editor | CodeMirror 6 via @uiw/react-codemirror |
| Markdown | react-markdown + remark-gfm |
| Iconos | lucide-react |
| Media Metadata | ffprobe (FFmpeg) |
- Docker — la base de datos corre en un contenedor, y la red de pruebas entera también
- Go 1.25+
- Bun 1.3+ o Node.js 18+
- ffprobe (para auto-detección de metadatos multimedia)
Para solo levantar la red de pruebas alcanza con Docker: ver Red de prueba en Docker.
Para ver el sistema distribuido completo sin instalar nada más que Docker, saltar a Red de prueba en Docker.
# 1. Clonar el repositorio
git clone <url>
cd radix
# 2. Iniciar el backend
cd backend
cp .env.example .env # Configurar puerto si es necesario
go run ./cmd/server # Servidor en :1323
# 3. En otra terminal, iniciar el frontend
cd frontend
cp .env.example .env
bun install
bun run dev # SPA en :5173 (proxy automático al backend)Abrir http://localhost:5173 e iniciar sesión (ver credenciales abajo) o entrar como invitado.
Usuarios creados por go run ./cmd/seed. Todos comparten la misma contraseña, solo cambia el email:
| Rol | Password | |
|---|---|---|
| Admin | carlos.mendoza@radix.local |
radix2024 |
| Student | sofia.ramirez@radix.local |
radix2024 |
| Student | mateo.torres@radix.local |
radix2024 |
Invitado no requiere credenciales (botón "Entrar como invitado" en el login).
| Variable | Default | Descripción |
|---|---|---|
PORT |
1323 |
Puerto del servidor Go |
NODE_ID |
hostname | Identidad de este nodo; se graba en cada fila y resuelve los empates al fusionar datos de otro nodo |
SYNC_PEERS |
vacío | URLs base de los nodos pares de los que tirar operaciones, separadas por coma. Vacío = nodo aislado |
SYNC_TOKEN |
vacío | Secreto compartido para GET /sync/ops. Vacío cierra ese endpoint |
SYNC_INTERVAL_SECONDS |
60 |
Cada cuánto se intenta una ronda de sincronización |
ENV_FILE |
.env |
Archivo de configuración a cargar; permite correr un segundo nodo desde el mismo checkout |
LOG_BUFFER_SIZE |
200 |
Máximo de líneas en el ring buffer de logs |
CORS_ORIGINS |
* |
Orígenes CORS permitidos (separados por coma) |
ENVIRONMENT |
development |
Entorno (development / production) |
LOG_RETENTION_DAYS |
30 |
Días de retención de logs en el historial buscable |
| Variable | Default | Descripción |
|---|---|---|
VITE_API_PORT |
1323 |
Puerto del backend para el proxy de Vite |
radix/
├── backend/ # Backend Go
│ ├── cmd/
│ │ ├── server/main.go # Entry point: grafo de dependencias con uber/fx
│ │ ├── seed/main.go # Datos demo, o `-zip` para importar un respaldo
│ │ └── flush/main.go # Vacía todas las tablas dejando el esquema
│ ├── internal/
│ │ ├── config/config.go # Carga de .env + struct de configuración
│ │ ├── models/models.go # Tipos de datos (User, Lesson, Quiz, LibraryItem...)
│ │ ├── database/ # Migraciones goose + esquema + código generado por sqlc
│ │ ├── store/
│ │ │ ├── store.go # Adaptador sqlc ↔ models, y sesiones en memoria
│ │ │ ├── backup.go # Export/import genérico por reflexión, fusión por versión
│ │ │ ├── version.go # Columnas (hlc, origin_node) y regla de conflicto
│ │ │ └── ops.go # Cola DTN: registrar, aplicar y reenviar operaciones
│ │ ├── clock/hlc.go # Reloj lógico monótono (el Pi no tiene RTC ni NTP)
│ │ ├── dtn/syncer.go # Tira de los nodos pares y baja los archivos que falten
│ │ ├── backupzip/ # Lectura del zip de respaldo (la usan el handler y el seed)
│ │ ├── seed/seed.go # Datos de prueba realistas (3 usuarios, 3 cursos, 6 lecciones...)
│ │ ├── auth/auth.go # Login email/password (bcrypt) + guest + sesiones + middleware RBAC
│ │ ├── middleware/
│ │ │ ├── logger.go # Ring buffer en memoria + middleware de logging por request
│ │ │ ├── observability_core.go # Core de zap que unifica stdout + tail en vivo + DB en un solo log call
│ │ │ └── log_persister.go # Batching async a server_logs + limpieza por retención
│ │ └── handlers/ # Handlers REST por entidad
│ │ ├── handlers.go # Struct Handler + RegisterRoutes()
│ │ ├── library.go # Upload multipart + ffprobe + detail + file serve + usage
│ │ ├── courses.go # CRUD de cursos y lecciones
│ │ ├── quizzes.go # Creación y corrección de quizzes
│ │ ├── monitor.go # Métricas y cola DTN
│ │ ├── backup.go # Exportar/importar la base como zip
│ │ ├── sync.go # Rutas que consume otro nodo (token compartido)
│ │ └── logs.go # Tail en vivo + historial filtrable + stats
│ ├── uploads/ # Archivos subidos (gitignored)
│ ├── Dockerfile # Solo para la red de pruebas
│ ├── .env / .env.example / .env.b.example
│ └── go.mod
│
├── frontend/ # Frontend React + Vite
│ ├── src/
│ │ ├── types/index.ts # Interfaces TS espejo de los structs Go
│ │ ├── lib/
│ │ │ ├── api.ts # Cliente fetch tipado con auth token
│ │ │ ├── markdown.ts # Parser de [[id]] + extractor de TOC
│ │ │ ├── codemirror-wiki.ts # Extensiones CodeMirror (highlight + tooltip para [[id]])
│ │ │ └── rbac.ts # Helpers de permisos por rol
│ │ ├── context/AuthContext.tsx # Estado global de autenticación
│ │ ├── components/
│ │ │ ├── layout/ # Header + NavItems, RootLayout, ReadingLayout, LessonSidebar
│ │ │ ├── common/ # ProtectedRoute, BackLink, AppearancePanel, modales de selección
│ │ │ ├── admin/ # MonitorPanel y LogsPanel (pestañas del Panel Admin)
│ │ │ ├── ui/ # Card, Button, ProgressBar, Badge
│ │ │ ├── InlineMedia.tsx # Renderiza media embebido según tipo
│ │ │ └── MarkdownEditor.tsx # Editor CodeMirror 6 con toolbar
│ │ └── pages/
│ │ ├── Login.tsx # Form email/password + botón invitado
│ │ ├── Library.tsx # Grid con filtros + upload
│ │ ├── LibraryDetail.tsx # Preview + metadatos + editar
│ │ ├── Courses.tsx # Lista de cursos
│ │ ├── CourseDetail.tsx # Lecciones + botones crear/editar
│ │ ├── LessonViewer.tsx # Visor con media embebido + quiz + sidebar
│ │ ├── LessonEditor.tsx # Editor dedicado (crear/editar)
│ │ ├── student/Dashboard.tsx # Progreso, XP, medallas
│ │ └── admin/AdminPanel.tsx # Tres pestañas: General, Monitor y Logs
│ ├── Dockerfile / nginx.conf.template # Solo para la red de pruebas
│ ├── .env / .env.example
│ └── package.json
│
├── config/docker/
│ ├── docker-compose.yml # Base de datos para desarrollo
│ └── testnet.yml # Red de pruebas: N nodos completos, efímeros
├── seeds/ # Zip semilla + el generador que lo produce
├── docs/ # Informes para la entrega universitaria
├── testnet.sh # Recrea la red de pruebas y muestra las URLs
└── README.md
Todas las rutas bajo /api/v1/. Autenticación vía Authorization: Bearer <token> o ?token=<token> (para media embebido).
| Método | Ruta | Rol | Descripción |
|---|---|---|---|
POST |
/auth/login |
público | Login: {"email", "password"} → {token, user} |
POST |
/auth/guest |
público | Login invitado (sin credenciales) → {token, user} |
POST |
/auth/logout |
auth | Invalida sesión |
GET |
/library |
auth | Lista items (?type=&category= filtros) |
GET |
/library/:id |
auth | Detalle del item |
PATCH |
/library/:id |
admin | Editar título/categoría |
GET |
/library/:id/file |
auth | Servir archivo (soporta ?token= para media) |
GET |
/library/:id/usage |
auth | Lecciones que enlazan este archivo vía [[id]] (calculado en vivo) |
POST |
/library |
admin | Subir archivo (multipart/form-data, game=true marca un HTML como videojuego) |
GET |
/courses |
auth | Lista cursos |
POST |
/courses |
admin | Crear curso |
GET |
/courses/:id |
auth | Curso + lecciones (estudiante: solo si está inscrito) |
GET |
/courses/:id/students |
admin | Estudiantes inscritos + puntos |
GET |
/courses/:id/students/available |
admin | Estudiantes no inscritos, para el picker |
POST |
/courses/:id/students |
admin | Inscribir estudiante |
DELETE |
/courses/:id/students/:userId |
admin | Desinscribir estudiante |
GET |
/courses/:id/resources |
auth | Archivos de biblioteca enlazados desde el curso |
GET |
/courses/:id/forum |
auth | Hilos y respuestas del foro del curso |
GET |
/courses/:id/forum/links |
auth | Recursos enlazados desde los posts del foro |
POST |
/courses/:id/forum |
auth | Crear hilo o respuesta |
POST |
/forum/:id/like |
auth | Dar like a un post |
DELETE |
/forum/:id/like |
auth | Quitar like |
POST |
/courses/:id/lessons |
admin | Crear lección |
GET |
/courses/:cId/lessons/:lId |
auth | Lección + quiz adjunto (oculto a guest) |
PUT |
/lessons/:id |
admin | Editar lección (título + contenido) |
GET |
/lessons |
auth | Todas las lecciones (para el picker de [[id]]) |
GET |
/lessons/:id/links |
auth | Archivos/lecciones/quizzes que esta lección enlaza |
GET |
/lessons/:id/usage |
auth | Lecciones que enlazan esta lección vía [[id]] |
GET |
/courses/:id/quizzes |
auth | Quizzes del curso |
POST |
/quizzes |
admin | Crear quiz con preguntas, asociado a un curso |
GET |
/quizzes/:id |
auth† | Ver quiz (†no guest) |
PUT |
/quizzes/:id |
admin | Editar quiz |
GET |
/quizzes/:id/links |
auth | Archivos/lecciones que este quiz enlaza |
POST |
/quizzes/:id/submit |
student | Responder → corrige y registra la nota |
GET |
/monitor |
admin | Métricas (disco, usuarios, cola DTN, nodos pares) |
POST |
/monitor/sync |
admin | Ejecutar una ronda de sincronización ahora |
GET |
/sync/ops |
nodo par | Registro de operaciones desde un cursor (?since=&limit=&node=), autenticado con SYNC_TOKEN |
GET |
/sync/file/:id |
nodo par | Archivo de un item de biblioteca, para que el nodo par complete lo que la operación no lleva |
GET |
/backup/export |
admin | Exportar toda la base como zip |
POST |
/backup/import |
admin | Importar un zip (merge por versión de fila) |
GET |
/logs |
auth | Últimas N líneas del log del servidor (tail en vivo) |
GET |
/logs/history |
admin | Historial filtrable (?level=&from=&to=&q=&limit=&offset=) |
GET |
/logs/stats |
admin | Conteos por nivel + retención configurada (?from=&to=) |
Cada servidor de borde anota todo lo que escribe como una operación reproducible en otro nodo: qué tabla, qué fila, qué versión y la fila entera. Un nodo par pide las operaciones que le faltan (GET /sync/ops?since=N) en vez de mandarse la base completa.
- Store-and-forward. Nada espera a la red: la escritura se guarda con su operación al lado, y sale cuando hay enlace. Si no lo hay, se reintenta en la ronda siguiente, indefinidamente.
- Tira, no empuja. Cada nodo lleva su propio cursor sobre el registro del otro. El que tiene energía y señal es el que inicia.
- Idempotente. La identidad de una operación es
(nodo de origen, tabla, clave, versión), con un índice único detrás: recibirla dos veces no hace nada. - Se reenvía. Una operación aplicada se guarda conservando su origen, así que A → B → C converge sin que A y C se vean nunca.
- Conflictos por versión. Si dos nodos editaron la misma fila, gana la de
(hlc, origin_node)mayor. Elhlces un reloj lógico monótono: un Raspberry Pi sin RTC ni NTP tiene la hora mal, y un simple timestamp resolvería los conflictos al revés. - Los borrados viajan. Son operaciones como cualquier otra, y el propio registro hace de lápida: una edición vieja que llega después de un borrado no resucita la fila.
El respaldo ZIP sigue existiendo para lo que un delta no puede hacer: arrancar un nodo nuevo desde cero, y mover contenido a mano cuando no hay red.
- Los archivos viajan aparte. Una operación lleva la fila de la biblioteca, no los bytes. Después de aplicar operaciones, el nodo compara su inventario contra la carpeta
uploads/y se baja lo que le falte (GET /sync/file/:id). Como compara el inventario entero y no solo lo recién llegado, también repara una transferencia que se cortó a la mitad.
Techo conocido: las operaciones no se podan todavía. Un nodo desconectado más tiempo del que se conserven tendrá que arrancar otra vez desde un ZIP.
Varios servidores de borde completos — base de datos, backend y frontend cada uno — hablando entre sí en la misma máquina. No es persistente a propósito: sirve para ver el sistema distribuido funcionando y para las capturas del informe, no para trabajar.
./testnet.sh # recrea el entorno y muestra las URLs
./testnet.sh --three # 3 nodos (A -> B -> C)
./testnet.sh --no-build # sin reconstruir imágenes
./testnet.sh --open # además abre los frontendsEl script baja lo que hubiera, vuelve a levantar todo, espera a que cada nodo responda de verdad (aplicar migraciones e importar el zip lleva unos segundos) y recién ahí imprime las direcciones. Es repetible: correrlo otra vez deja el entorno como recién nacido.
A mano, si preferís:
docker compose -f config/docker/testnet.yml up --build # 2 nodos
docker compose -f config/docker/testnet.yml --profile three up # 3 nodos
docker compose -f config/docker/testnet.yml down # no queda nada| Nodo | Frontend | Backend | NODE_ID |
|---|---|---|---|
| A | http://localhost:5173 | :1323 |
edge-a |
| B | http://localhost:5174 | :1324 |
central |
C (perfil three) |
http://localhost:5175 | :1325 |
edge-c |
Las bases de datos no publican puertos: solo se llega a ellas a través de su propio backend, igual que en el equipo real. Los datos de sqld y los archivos subidos viven en tmpfs, así que apagar el entorno lo borra entero; cada arranque vuelve a importar el mismo zip semilla, con lo que los tres nodos parten del mismo contenido y de ahí divergen.
Entrar con cualquier correo @radix.edu del zip y la contraseña radix2024 (el docente es elena.vasquez@radix.edu). El intervalo de sincronización está en 15 s en vez de 60 para no quedarse esperando.
Qué mirar:
- Editá una lección en el nodo A y recargá el nodo B: aparece corregida.
- Subí un archivo en A y abrilo en B — la operación lleva la fila, y el archivo se descarga aparte.
- Parala:
docker compose -f config/docker/testnet.yml stop backend-b. Seguí trabajando en A y mirá crecer la cola en Panel Admin → Monitor. Al hacerstart backend-b, se vacía sola. - Con el tercer nodo, C solo tira de B y B no sabe que C existe: lo escrito en A igual llega a C, reenviado por B. (Por lo mismo, el enlace de C es de una sola dirección: lo que se escriba en C se queda ahí.)
Para iterar sobre el código con recarga en caliente, sin contenedores para la aplicación:
# Base de datos del segundo nodo
cd config/docker && docker compose --profile peer up -d
# Configuración del segundo nodo
cp backend/.env.b.example backend/.env.b # PORT=1324, NODE_ID=central, :8081
# En backend/.env (primer nodo): NODE_ID=edge-a, SYNC_PEERS=http://localhost:1324
# y el mismo SYNC_TOKEN en los dos.
cd backend && ENV_FILE=.env.b go run ./cmd/server # nodo B
cd backend && go run ./cmd/server # nodo A (otra terminal)
cd frontend && bun run dev # UI del nodo A
cd frontend && VITE_API_PORT=1324 bun run dev --port 5174 # UI del nodo BEste camino usa la base del segundo nodo dentro de Docker pero corre backend y frontend en el host, con recarga en caliente. Para solo ver el sistema funcionando, la red de prueba de arriba es menos trabajo.
| Funcionalidad | Admin | Student | Guest |
|---|---|---|---|
| Dashboard personal con XP | — | ✅ | — |
| Explorar cursos y lecciones | ✅ | ✅ (solo inscritos) | ✅ |
| Crear / editar cursos, inscribir estudiantes | ✅ | — | — |
| Crear / editar lecciones | ✅ | — | — |
| Crear / editar quizzes | ✅ | — | — |
| Subir / editar archivos (incl. videojuegos HTML) | ✅ | — | — |
| Ver quizzes | ✅ | ✅ | ❌ |
| Responder quizzes | — | ✅ | — |
| Participar en el foro del curso | ✅ | ✅ | ✅ |
| Ganar XP | — | ✅ | — |
| Monitor del servidor | ✅ | — | — |
| Logs (tail en vivo + historial + stats) | ✅ | ❌ | ❌ |
| Exportar / importar respaldo | ✅ | — | — |
type User struct {
ID, Name, Email string
PasswordHash string // no serializado
Role Role // "admin" | "student" | "guest"
CompletedLessons []string
EnrolledCourses []string // solo relevante para student
}
// Fila de la pestaña "Estudiantes" de un curso.
type CourseStudent struct {
ID, Name, Email string
Points int // suma en vivo de quiz_grades para ese curso
}
type LibraryItem struct {
ID, Title, Type, Category string // Type: video|audio|image|pdf|text|document|game
SizeKB int
MimeType, OriginalFilename string
UploadedBy, UploadedAt, ModifiedAt string
Duration, Resolution string // auto-detectados por ffprobe
FilePath string // interno, no serializado
}
type Lesson struct {
ID, CourseID, Title, ContentText string
QuizID *string // quiz adjunto vía quizzes.lesson_id, no vía [[id]]
}
// Quiz.LessonID es opcional: vive en el curso ("Cuestionarios") y
// opcionalmente se adjunta a una lección.
type Quiz struct {
ID, CourseID, Title, Description string
LessonID *string
Value int // puntos máximos; la nota = value * (aciertos / total)
Questions []QuizQuestion // { Text, Options[], CorrectIndex }
}
// Un hilo o una respuesta del foro de un curso (árbol vía ParentID).
type ForumPost struct {
ID, CourseID string
ParentID *string
UserID, AuthorName string
AuthorRole Role
Title, Body, CreatedAt string
LikeCount int
Liked bool // si el usuario que pide lo likeó
}
type ServerLog struct {
ID int64
Timestamp, Level, Message string
Fields string // JSON genérico — método/path/rol/status/duración para requests, lo que sea para otros logs
}
type Course struct {
ID, Title, Description, Category string
}
// Una operación: lo que viaja entre nodos. Identidad = (OriginNode, Table, PK, HLC).
type SyncOp struct {
Seq int64 // orden local; cambia al reenviarse
OriginNode string // quién la produjo primero, se conserva
Table, PK string
Op string // "upsert" | "delete"
HLC int64 // versión: reloj lógico monótono
Payload map[string]any // la fila entera (vacío en un delete)
Label, CreatedAt string
}
type SyncQueue struct {
TransactionCount int // operaciones que ningún nodo par confirmó leer
Logs []string
Peers []SyncPeer // { Peer, NodeID, LastSeq, LastSyncAt, LastError }
}| Ruta | Componente | Rol | Descripción |
|---|---|---|---|
/login |
Login | público | Login email/password + acceso invitado |
/dashboard |
StudentDashboard | student | Progreso, XP, medallas (umbrales fijos en cliente) |
/library |
Library | todos | Grid de archivos con filtros |
/library/:id |
LibraryDetail | todos | Preview + metadatos + editar (admin) |
/courses |
Courses | todos | Lista de cursos |
/courses/:id |
CourseDetail | todos | Lecciones, quizzes, estudiantes (admin) y foro del curso |
/courses/:id/lessons/new |
LessonEditor | admin | Editor completo con CodeMirror |
/courses/:id/lessons/:lid/edit |
LessonEditor | admin | Editar lección existente |
/courses/:id/lessons/:lid |
LessonViewer | todos | Visor con media embebido + quiz adjunto |
/courses/:id/quizzes/new |
QuizEditor | admin | Crear quiz del curso |
/courses/:id/quizzes/:qid/edit |
QuizEditor | admin | Editar quiz |
/courses/:id/quizzes/:qid |
QuizViewer | todos† | Ver/responder quiz (†no guest) |
/courses/:id/forum/:postId |
ForumThread | todos | Hilo del foro con respuestas |
/admin |
AdminPanel | admin | Pestañas: General (crear curso), Monitor y Logs |
/admin/monitor |
(redirect) | admin | → /admin?tab=monitor |
/admin/logs |
(redirect) | admin | → /admin?tab=logs |
Dentro del contenido de cualquier lección (en markdown), puedes enlazar archivos de la biblioteca usando la sintaxis:
# Introducción a la Biología
Lee [[lib1]] para una introducción completa.
Luego mira [[lib2]] y escucha [[lib3]] como material complementario.Cada [[id]] se renderiza automáticamente como media embebido:
| Tipo de archivo | Renderizado |
|---|---|
video |
<video controls> con streaming |
audio |
<audio controls> |
image |
<img> con preview |
pdf |
<iframe> embebido |
text |
<pre> con fetch del contenido |
document |
Card con icono + descarga |
En el editor, los [[id]] se resaltan con syntax highlighting (CodeMirror) y al hacer hover se muestra un tooltip con los metadatos del archivo. La barra lateral derecha lista todos los archivos enlazados en tiempo real.
La página de detalle de un archivo en la Biblioteca muestra en qué lecciones se usa — calculado en vivo buscando [[id]] en el contenido de las lecciones (sin tabla de relación separada), así que si editás o eliminás una lección se refleja automáticamente.
Este prototipo cubre el nodo educativo (LMS offline-first + cola DTN entre nodos). Lo que el macroproyecto propone y todavía no está construido:
Software
- Medallas/logros reales. Hoy
StudentDashboardcalcula 3 medallas con umbrales fijos en el cliente (completedCount >= 1/3/6, contotalLessons = 6hardcodeado) — no hay backend detrás ni escala con las lecciones reales. - TTL de sesiones. Las sesiones no expiran (
Storeno tiene eviction, solo el snapshot asessions.json). - Filtrado de logs por
fields. El JSON estructurado deserver_logs.fieldsse guarda pero no se puede filtrar por él (solo nivel/fecha/texto libre). - Suite de pruebas más completa. Hay tests de store/sync/backup/HLC (
internal/store,internal/handlers/sync_test.go), pero ningún test de handlers HTTP end-to-end ni de frontend.
Infraestructura (fuera del alcance de este prototipo software)
- CRDTs reales. La resolución de conflictos actual es last-writer-wins por fila vía reloj lógico (HLC) — no fusión a nivel de campo.
- IPFS para almacenamiento de contenido direccionado por hash.
- Clúster de 3 microservidores por escuela, red mesh Wi-Fi física, y gateway LoRaWAN de telemetría.
(Agrega capturas aquí)
Proyecto académico — Universidad.