Skip to content

Add AI student assistant, AI usage analytics and secure CSV progress - #102

Merged
JohannTF merged 79 commits into
mainfrom
feature/ai-assistant
Nov 10, 2026
Merged

JohannTF merged 79 commits into
mainfrom
feature/ai-assistant

Conversation

@IrminDev

@IrminDev IrminDev commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Description

Adds an educational AI assistant for students, an AI policy per assignment, and AI usage analytics for students, group owners and administrators. It also secures the CSV import progress socket, reworks the student workspace and teacher analytics, and fixes Java timeouts being reported as memory errors.

Fixes: no linked issue.

Reviewing the diff. GitHub lists 79 commits, but 53 of them already reached main through the squash of #89. The changes in this PR come from the other 26, starting at f1df115. graphify-out/ accounts for 3.2 million of the 3.25 million added lines. Without it, the diff is 292 files, +16,561 / -2,120:

git diff origin/main...origin/feature/ai-assistant -- . ':!graphify-out'

AI assistant

  • Student API under /api/assignments/{assignmentId}/assistant: availability, POST /interactions (10 requests per minute) and conversation history.
  • Every request goes through input review, generation and output review (educational-assistant-v4), restricted by the assignment's assistance level.
  • Replaying a clientRequestId with the same payload creates no new interaction and charges no extra quota. Interrupted requests can be recovered (RF-11).
  • Provider: Spring AI Google GenAI 1.1.8 with google-genai 1.76.0, default model gemini-3.5-flash-lite. Spring and SDK retries are disabled, and provider calls are capped per minute.
  • The assistant is off by default, so the backend starts without AI credentials.
  • Assistant text is erased when a group is archived or deleted (AssistantTextPurgeService).

AI policy per assignment

Teachers set availability, a request limit (1 to 10 when enabled, 0 when disabled) and a level (CONCEPTUAL_ONLY, EXPLANATIONS_AND_GUIDING, EXPLANATIONS_GUIDING_AND_SNIPPETS) when creating, cloning or editing an assignment. Existing assignments use PUT /api/assignments/{id}/ai-policy.

AI usage analytics

Audience Endpoints Access
Student /api/assistant-usage/me... Own usage
Group owner /api/groups/{groupId}/assistant-usage..., /api/assignments/{assignmentId}/assistant-usage..., /api/assistant-usage/owned-groups Owned groups
Administrator /api/admin/assistant-usage..., /api/admin/users/{userId}/assistant-usage... ADMIN and CHECK_ANALYTICS; per-user views also need VIEW_USERS

Model calls are recorded in assistant_model_calls without prompts, context, code, HTTP bodies or generated text. This PR also adds GET /api/groups/{groupId}/metrics/students/{studentId}/assignments.

Authentication and CSV import

  • POST /api/auth/websocket-ticket issues a single-use ticket, valid for 60 seconds and bound to the user, which the /ws/csv-progress handshake requires.
  • CSV imports are queued under their owner and start only after that owner subscribes. Unclaimed tasks expire after 5 minutes.
  • PUT /api/auth/me/password revokes existing JWTs and returns a replacement session.

Frontend

  • Student: the assistant is docked as a collapsible right panel with chat, history and usage views. The editor keeps one draft buffer per language and, on entry, restores the local draft, then the delivered code, then the template. Submission status and the replacement flow are clearer. Answers highlight code with prismjs.
  • Teacher: AI policy fields in assignment forms. Analytics now lead with KPIs and items that need attention, use sortable paged tables, and have a separate AI usage tab.
  • Administrator: AI usage dashboard at /admin/ai-usage.
  • Public: /privacy and /terms pages.
  • Layout: responsive dashboards with a mobile navigation menu, a redesigned sign-in and password recovery screen, and a honeycomb background on the landing hero.

Worker

  • Java runs as java -Xrs Main, so SIGTERM from timeout ends the JVM directly. Before, the JVM signal handler could fail at the PID limit and escalate to SIGKILL (137), which was reported as MLE instead of TLE.
  • Reference solution failures now report the test position, verdict-specific guidance, exit code, time and memory.
  • C and C++ containers now allow 32 PIDs. With 8, docker exec failed intermittently on the deployed workers because runc also runs inside the container's limit, which produced false RTE results with exit code 128.

Configuration and documentation

  • The development docker-compose.yaml moves MinIO to Chainguard images.
  • SQL logging is off, DispatcherServlet logs at DEBUG, and the Tomcat access log is written to ${user.dir}/logs.
  • New documents in docs/ai-assistant/ cover requirements, business rules, sequence diagrams, usage implementation and test records. llms/ is updated.

Google Cloud deployment

  • Dockerfiles and .dockerignore files for the backend and the worker, firebase.json for the frontend, and .gcloudignore for Cloud Build.
  • deploy/gcp/: Cloud Build configuration, image build script, queue metric publisher and VM startup scripts. They contain no secrets or project identifiers; those values come from instance metadata and Secret Manager at startup.
  • MINIO_REGION lets the existing storage client work with Cloud Storage. Local MinIO behaves as before when it is not set.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Style/UI update (formatting, styling, etc.)
  • Test update

Breaking for API clients:

  • PUT /api/auth/me/password returns AuthResponse in data instead of null, and previous JWTs stop working.
  • /ws/csv-progress requires a ticket query parameter, and CSV imports do not start until the owner subscribes.

How Has This Been Tested?

  • Unit tests
  • Integration tests
  • Manual testing
Check Where Result at 056a7d1
Backend ./gradlew test (H2) Backend CI 558 tests, 560 runs, all passed
Worker test selection Worker CI 42 tests, all passed
Build, code quality and dependency checks CI/CD Pipeline Passed
Frontend typecheck, Vitest and build Local Passed; 60 tests in 20 files
CodeQL GitHub Analysis jobs passed; the gate failed with 1 new high-severity alert

The commits that follow 056a7d1 add packaging files, the optional MINIO_REGION setting, the new C and C++ PID limit and the frontend layout changes. At 732af02, every CI job passed again except the CodeQL gate.

Not covered:

  • No end-to-end run against real PostgreSQL, RabbitMQ, MinIO and the sandbox was done on the merged tree, because Docker was unavailable locally.
  • postgresAssistantTest and liveAssistantTest are excluded from ./gradlew test and have no recorded run.
  • The full worker suite needs Docker and was not rerun.
  • The deployment files were used for the current Google Cloud deployment, but they have no automated tests, and the container images were not built locally.
  • Manual testing is partial. docs/ai-assistant/AI-TEST.md records prompt-injection attempts and responses. The MAN-* cases in AI_ASSISTANT_TESTS.md, AI_REQUEST_RETRY_EVIDENCE.md and CH4_RNF_EVIDENCIAS.md are guides with no recorded results.

Test Configuration:

  • Java version: 21
  • PostgreSQL version: 18.1 in docker-compose.yaml; automated suites use in-memory H2
  • OS: macOS 27.2 arm64 for the local checks; Ubuntu on GitHub Actions

Checklist

  • My code follows the style guidelines of this project. assistant-usage/api.ts and AdminAiUsagePage.tsx import other features' internals, which CLAUDE.md does not allow.
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings. Not verified.
  • I have added tests that prove my fix is effective or that my feature works. 24 new backend test classes.
  • New and existing unit tests pass locally with my changes. Backend and frontend pass, and the worker selection passes in CI; the full worker suite was not rerun. See the removed tests below.
  • Any dependent changes have been merged and published. None required.

Test Coverage

Backend JaCoCo report at 056a7d1. Worker and frontend coverage are not reported.

  • All new code is covered by tests. Lowest: AssistantUsageRepository 3.35%, AdminAssistantUsageController 4.35%, WebSocketTicketHandshakeInterceptor 16.22%, OwnerAssistantUsageController 29.24%.
  • Overall coverage is at least 70%. Reported at 71.79% (-6.58).
  • Coverage for changed files is at least 80%. Reported at 71.38%.

Screenshots (if applicable)

None attached.

Additional Notes

Merge blocker: CodeQL

CodeQL reports one new high-severity alert (#3), "Clear text storage of sensitive information", at codehive-frontend/app/core/storage/token.storage.ts:4. The code_scanning rule on main blocks the merge while it stays open. It is a false positive: the stored value is response.data.token, the JWT returned by updatePassword, not the password. Login stores its token through the same function.

Alert #1 (java/spring-disabled-csrf-protection) already exists on main and is not part of this gate.

Recovered work from main

Merge commit c00dbb9 resolved overlaps in favor of this branch. Because #89 was a squash, Git also treated later work from backup/no-student-stats as overlapping, and merging at that point would have removed it from main. Commit 056a7d1 merges backup/no-student-stats from the real fork point (5354b39), which recovers:

  • Bounded RabbitMQ listener retries and 404 responses for unknown API routes.
  • Audit entries for user creation and CSV uploads.
  • Grades kept when an identical test suite is resubmitted, and the in-flight update state (pendingUpdate).
  • Group restoration when CREATE_GROUP is regained, deleted groups visible to their owner, and guarded restoration.
  • Group management for students with CREATE_GROUP (GroupManagementRoute, ManagedGroupsSection) and role protection on /dashboard.
  • Restoring delivered code through GET /api/submissions/mine/{submissionId}/source.
  • Teacher and admin UI fixes: ActionMenu, archived groups in the gradebook, the assignment status drawer and list context, the clickable admin row, and role-aware enrollment.

Where both sides implemented the same thing:

Topic Decision
Assignment.setExamples Keeps this branch's reuse by explicit order, plus @OrderBy from main
Editor drafts Keeps code-drafts.ts and useCodeDraft, adds delivered-code restore from main, and removes the superseded code-draft.ts and language-switch.ts
Deleted groups and assignments Terminal in the teacher UI, as on main; the backend keeps its restore endpoints
Group hero menu Uses ActionMenu from main, keeps the Analytics link, and leaves join-code rotation in the Share dialog

Removed tests compared with main:

  • Frontend: admin/api/client.test.ts, admin/utils/permissions.test.ts and student/api/metrics.api.test.ts, deleted in cbbfc46. code-draft.test.ts and language-switch.test.ts go away with their modules, and code-drafts.test.ts covers the initial-code rules.
  • Backend: CI reports 5 test names removed, renames included. They cover the previous password response and the previous CSV flow, which this PR replaces.

Open review findings

Of the 8 active Copilot threads, the ones on AssignmentDTO.java, GlobalExceptionHandler.java and StudentSidebar.tsx report behavior that 056a7d1 recovers. Still open in the code:

  • High severity: "Atomically claim CSV tasks before execution" and "Bound queued upload jobs and retained bytes", both in the CSV flow.
  • Feature-boundary imports in assistant-usage/api.ts and AdminAiUsagePage.tsx.
  • BackLink falling back incorrectly when the page is opened directly.

They do not block the merge under the current rules.

Deployment notes

  • To enable the assistant, set ASSISTANT_ENABLED=true and ASSISTANT_MODEL_PROVIDER=google-genai. Related settings: GEMINI_API_KEY, GEMINI_MODEL, ASSISTANT_MODEL_MAX_OUTPUT_TOKENS, ASSISTANT_MODEL_TEMPERATURE, ASSISTANT_MODEL_TIMEOUT_SECONDS and ASSISTANT_PROVIDER_CALLS_PER_MINUTE.
  • No Flyway migration is included. ddl-auto=update creates the new tables (assignment_ai_policies, assistant_conversations, assistant_interactions, assistant_model_calls), but not the partial indexes and checks in docs/ai-assistant/AI_USAGE_DEV_SCHEMA.sql, such as ux_assistant_one_pending. Apply that script in each environment once the tables exist.

Repository size

graphify-out/ grows from 135 MB to 285 MB in the repository.

IrminDev and others added 30 commits July 19, 2026 00:27
* Added base for RabbitMQ integration and added worker application to eecute the code safely

* Updated AGENTS.md and added missing files

* Added the main model and folder structure proposal for the bucket

* Tunned the execution logic to allow executions without submitting the code

* Added base logic for remote code execution and communication using rabbit MQ. Also MinIO was implemented

* Added jakarta persistance, DTOs, mappers and repositories for code execution

* Enhanced the worker response

* Added code execution functionality in the backend, added logs for better understanding in the workflow and fixed business logic

* Fixes for business logic

* Moved ExecutionResult according to its context

* Added max memory use via statistics from docker api

* Minor fixes

* Updated AGENTS.md file optimizing the token consumption

* Changed the use of Long ids for UUIDs

* Updated assignment logic, updated documentation

* Added useful files

* Minor fixes

* Added security features and added endpoints for execution logic

* Added enrollment number validation and minor fixes

* redesigne of login, forgot-password pages, index Hero.tsx. and implement sandbox monako

* Corrected landing and added teacher pages structure

* Moved web code editor to student context, added execution logic

* Implement resizable panels for the AssignmentPage.

* Refactor admin and teacher layouts to use AppHeader component;.

* Redesign auth, landing, and admin panels with new UI system

- Apply new design to LoginPage, RecoveryPasswordPage (both states),
  Hero, Footer, Header, and HowItWorks landing components
- Add blinking cursor effect (.animate-blink) to code blocks in
  LoginPage and Hero
- Introduce AdminLayout component with icon-only sidebar and top bar
  (breadcrumb, search, bell, theme toggle)
- Redesign AdminDashboardPage: stat cards, quick actions, recent users
  table, system status panel with throughput chart
- Redesign CsvUploadPage: two-column layout with drop zone, inline
  upload button, progress section, CSV format reference, and tips panel
- Update CreateUserPage to use new AdminLayout shell

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Added claude.md

* feat: redesign teacher/student screens and add group management UI

- Redesigned teacher screens: dashboard, create-assignment, groups list, group detail, create-group, clone-assignment
- Added student screens: join-group, submission-history, execution-report
- Restored ProtectedRoute on all teacher (TEACHER role) and student (STUDENT role) routes
- Added group.types.ts and group.api.ts for student group data layer
- Self-contained dark layout per page with sidebar, tokens (azure/yellow/imperial/french/dark-*)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: JohannTF <trejofloresjohanndaniel@gmail.com>
Co-authored-by: Rodolfo-rgb <rodo.aparicio.lopez@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Addded unit and integraation tests in the backend
* Added base for RabbitMQ integration and added worker application to eecute the code safely

* Updated AGENTS.md and added missing files

* Added the main model and folder structure proposal for the bucket

* Tunned the execution logic to allow executions without submitting the code

* Added base logic for remote code execution and communication using rabbit MQ. Also MinIO was implemented

* Added jakarta persistance, DTOs, mappers and repositories for code execution

* Enhanced the worker response

* Added code execution functionality in the backend, added logs for better understanding in the workflow and fixed business logic

* Fixes for business logic

* Moved ExecutionResult according to its context

* Added max memory use via statistics from docker api

* Minor fixes

* Updated AGENTS.md file optimizing the token consumption

* Changed the use of Long ids for UUIDs

* Updated assignment logic, updated documentation

* Added useful files

* Minor fixes

* Added security features and added endpoints for execution logic

* Added enrollment number validation and minor fixes

* redesigne of login, forgot-password pages, index Hero.tsx. and implement sandbox monako

* Corrected landing and added teacher pages structure

* Moved web code editor to student context, added execution logic

* Implement resizable panels for the AssignmentPage.

* Refactor admin and teacher layouts to use AppHeader component;.

* Redesign auth, landing, and admin panels with new UI system

- Apply new design to LoginPage, RecoveryPasswordPage (both states),
  Hero, Footer, Header, and HowItWorks landing components
- Add blinking cursor effect (.animate-blink) to code blocks in
  LoginPage and Hero
- Introduce AdminLayout component with icon-only sidebar and top bar
  (breadcrumb, search, bell, theme toggle)
- Redesign AdminDashboardPage: stat cards, quick actions, recent users
  table, system status panel with throughput chart
- Redesign CsvUploadPage: two-column layout with drop zone, inline
  upload button, progress section, CSV format reference, and tips panel
- Update CreateUserPage to use new AdminLayout shell

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Added claude.md

* feat: redesign teacher/student screens and add group management UI

- Redesigned teacher screens: dashboard, create-assignment, groups list, group detail, create-group, clone-assignment
- Added student screens: join-group, submission-history, execution-report
- Restored ProtectedRoute on all teacher (TEACHER role) and student (STUDENT role) routes
- Added group.types.ts and group.api.ts for student group data layer
- Self-contained dark layout per page with sidebar, tokens (azure/yellow/imperial/french/dark-*)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(student): connect API endpoints and enforce business rules

- Add shared studentRequest client (student/api/client.ts) replacing
  duplicated authHeaders/parseResponse in each API file
- Refactor assignment, execution and group APIs to use the shared client
- Migrate StudentDashboardPage from mock getGroups() to real listMyGroups()
- Update GroupRow to use ClassGroup (isActive/archived) instead of Group
- Add launchDate, closeDate and validationStatus to Assignment type
- Disable Submit/Run in AssignmentPage when past closeDate, before
  launchDate, or validationStatus != READY; show inline status banner
- Fix SubmissionHistoryPage countdown to use closeDate (hard cutoff)
  and show dueDate separately with late indicator
- Add open/late/closed/upcoming state badges to StudentDashboardPage
  assignment rows; fix inprogress filter tab
- Align ClassGroup with backend pattern (isActive + archived fields)
- Remove dead groups.api.ts mock and getGroups() from dashboard.api
- Create missing dashboard/types/dashboard.types.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: JohannTF <trejofloresjohanndaniel@gmail.com>
Co-authored-by: Rodolfo-rgb <rodo.aparicio.lopez@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
and teacher features. Sandbox works as expected
* fix(security): derive rate-limit key from trusted client IP

- Problema: RateLimitAspect.getClientKey() tomaba el primer segmento de X-Forwarded-For, un header controlado por el cliente, sin verificar proxy de confianza.
- Vulnerabilidad mitigada: bypass total del rate limiting. Rotando el header se obtenía un bucket nuevo por petición, anulando los límites de login (fuerza bruta de credenciales), /recovery-password/reset (spraying de tokens) y /forgot (bombardeo). Era el único control anti-fuerza-bruta.
- Cambios: RateLimitAspect.java ahora usa request.getRemoteAddr() por defecto y solo honra X-Forwarded-For cuando ratelimit.trust-forwarded-for=true; nueva propiedad en application.properties (default seguro false, override por env RATELIMIT_TRUST_FORWARDED_FOR); nuevo RateLimitAspectTest (4 casos).
- Efecto secundario esperado: en tu despliegue detrás de proxy de confianza debes poner RATELIMIT_TRUST_FORWARDED_FOR=true en el entorno; si no, todos los clientes tras el proxy compartirían la IP del proxy y el límite sería global.

* fix(security): scope rate-limit buckets per endpoint

Problema: RateLimitService.resolveBucket() usaba computeIfAbsent con una clave que era solo la IP del cliente. El primer endpoint que tocaba una IP fijaba la capacidad del bucket, y el limit/duration de los demás endpoints se ignoraba.

Vulnerabilidad/defecto mitigado: los límites se cruzaban entre endpoints. Un estudiante que tocaba /api/execution/check (10/60) obtenía luego 10 intentos/min en /api/auth/login (inflación del límite más estricto); e inversamente, tocar /forgot (3/300) primero estrangulaba todos los endpoints para esa IP (DoS entre endpoints, agravado tras NAT).

Cambios:
- RateLimitAspect.java: la clave del bucket ahora es compuesta, firma-del-endpoint | IP (nuevo helper bucketKey(...)). Así cada endpoint obtiene su propio bucket, creado con su limit/duration correctos en el primer acceso. RateLimitService no necesitó cambios.
- RateLimitAspectTest.java: 2 pruebas nuevas (endpoints distintos → claves distintas; mismo endpoint+cliente → misma clave).

* fix(security): handle invalid JWT tokens without 500 responses

Problema: JWTAuthenticationFilter.doFilterInternal() llamaba a extractClaim(...), loadUserByUsername(...) y isTokenValid(...) sin capturar excepciones. Como el filtro corre antes del DispatcherServlet, esas excepciones (MalformedJwtException, SignatureException, ExpiredJwtException, UsernameNotFoundException) escapaban del alcance de @RestControllerAdvice y terminaban en HTTP 500 con traza.

Vulnerabilidad/defecto mitigado:
- Cada token expirado normal (expiración de 24h) devolvía 500 en vez de 401/403 → rompía el manejo de sesión del SPA.
- Oráculo 500-vs-4xx y amplificación de CPU/logs por tokens basura no autenticados.

Cambios (JWTAuthenticationFilter.java):
- El intento de autenticación se movió a un método privado authenticate(...) envuelto en un único try/catch (JwtException | IllegalArgumentException | UsernameNotFoundException); ante cualquiera de ellas se deja la petición sin autenticar y se continúa la cadena.
- filterChain.doFilter(...) se invoca exactamente una vez al final.
- Se eliminó la comprobación muerta userDetails == null (loadUserByUsername nunca devuelve null, lanza excepción).

* fix(security): redact private expected outputs from student execution reports

Problema: GET /api/execution/check/{id}/report devolvía el ExecutionReport verbatim. En entregas DEFINITIVE, el worker escribe en cada TestCaseResult la salida esperada del caso privado (en expectedOutput y embebida en feedback, p. ej. "Line N mismatch: expected 'X', got 'Y'"). Como el estudiante es el iniciador, authorizeExecutionRead le permitía leer esos datos.

Vulnerabilidad mitigada: fuga de las salidas esperadas de los casos de prueba privados al estudiante, contradiciendo llms/backend/groups/README.md ("Tareas y ejemplos", regla 4: casos y salidas esperadas son privados).

Cambios (ExecutionRequestService.java):
- Al servir el reporte, si la ejecución es DEFINITIVE y el lector no es el propietario del grupo, se limpian expectedOutput, actualOutput y feedback de cada TestCaseResult (se conservan estado, tiempos, memoria y los conteos globales). El propietario sigue viendo el reporte completo; las ejecuciones PRACTICE no se tocan (sus casos los aporta el propio estudiante).
- Refactor de apoyo: authorizeExecutionRead ahora recibe el User ya resuelto (nuevo helper requireAuthenticatedUser), y la comprobación de propietario se extrajo a isGroupOwner(...), reutilizada por la redacción. Se eliminó una búsqueda de usuario duplicada.

* fix(security): require explicit credentials for bootstrap super admin

Problema: AdminInitializer creaba un SUPER_ADMIN usando defaults committeados admin@codehive.com / Admin@12345 (en la anotación @value y en application.properties), con temporaryPassword = false.

Vulnerabilidad mitigada: un despliegue que olvidara configurar las variables arrancaba con una cuenta super admin de credenciales públicas conocidas (toma de control total). Verificado antes: el .env fija ADMIN_PASSWORD pero no ADMIN_EMAIL, por lo que el correo por defecto estaba activo.

Cambios:
- application.properties: eliminados los defaults inseguros; ahora son requeridos (${ADMIN_EMAIL:} / ${ADMIN_PASSWORD:}, sin literal).
- AdminInitializer: fail-fast al arranque si email o password están vacíos, y rechazo explícito del valor histórico Admin@12345. Sólo tras validar se comprueba la
existencia y se crea el admin. Se quitaron los literales inseguros

Efectos secundarios (acción requerida):
- ⚠️ Debes añadir ADMIN_EMAIL a tu .env (hoy no está); si no, el backend no arrancará (fail-closed intencionado). Igualmente, ADMIN_PASSWORD no puede ser
Admin@12345.
- El perfil test ya define email/password válidos, así que los tests no se ven afectados.
- Las cuentas admin ya existentes no se ven alteradas (el inicializdmin ya existe).

Prueba nueva (AdminInitializerTest): password en blanco / email en  → IllegalStateException sin guardar; config válida → creaSUPER_ADMIN; admin ya existente → no crea.

* fix(security): decouple CSV import execution from websocket message

 Problema: submitCsvJob solo encolaba un Runnable; el alta masiva de usuarios (privilegiada) se ejecutaba cuando cualquier cliente enviaba el taskId por el WebSocket (handleTextMessage → task.run()). El endpoint /ws/** es permitAll, así que un cliente no autenticado que conociera/filtrara el taskId disparaba la creación de cuentas; además, si nadie se conectaba, el job se descartaba en silencio a los 5 minutos.

Vulnerabilidad mitigada: ejecución de una operación privilegiada a través de un canal no autenticado, y pérdida silenciosa de la operación. También elimina el byte[] del CSV retenido en memoria durante el TTL.

Cambios:
- CsvRegistrationService.submitCsvJob: ahora ejecuta processAsync directamente en el executor @async (vía el proxy selfProvider) y devuelve el taskId. El WebSocket queda como canal de progreso únicamente.
- CsvProgressWebSocketHandler: eliminado todo el mecanismo de disparo (queueTask, pendingTasks, el ScheduledExecutorService, el TTL y @PreDestroy). handleTextMessage solo registra la sesión para recibir progreso.

Efectos secundarios: el progreso ahora es best-effort — el cliente debe abrir el WebSocket y suscribirse con el taskId justo tras recibir el 202; los mensajes emitidos antes de la suscripción se descartan (la operación en sí ya no depende del socket). El alta ocurre siempre, conecte o no el cliente.

Nota: el secuestro de canal / crecimiento no acotado del mapa (taskSessions) y la PII en los mensajes se abordan en los siguientes sub-incrementos A6b y A6c.

Prueba nueva (CsvRegistrationServiceTest): submitCsvJob ejecuta processAsync de inmediato y no interactúa con el handler WebSocket.

* fix(security): bound CSV progress websocket session state

Problema: handleTextMessage sobrescribía el único atributo taskId de la sesión en cada mensaje, pero afterConnectionClosed sólo eliminaba el último → una conexión que enviara N taskId distintos dejaba N−1 entradas huérfanas permanentes en taskSessions (cada una con referencia a la sesión). Sin validación del payload, un cliente anónimo podía insertar entradas arbitrarias/grandes → agotamiento de memoria.

Vulnerabilidad mitigada: DoS por crecimiento no acotado del mapa de sesiones desde un endpoint no autenticado.

Cambios (CsvProgressWebSocketHandler):
- Se valida que el payload sea un taskId con formato UUID (longitud 36 + UUID.fromString); los mensajes malformados se ignoran (rechaza payloads basura/grandes antes de almacenarlos).
- Una sola suscripción por sesión: al recibir un taskId nuevo se elimina el mapeo previo de esa sesión, de modo que el mapa no crece más de una entrada por conexión.
- El cierre y completeTask usan la eliminación condicional remove(key, session) (atómica) para no borrar el mapeo de otra sesión en una carrera.

Efectos secundarios: ninguno para el cliente legítimo (envía un único taskId UUID). El secuestro de canal por un tercero que conozca el taskId sigue mitigado únicamente por su aleatoriedad (UUID); reducir la PII expuesta en los mensajes es el siguiente sub-incremento A6c.

Prueba nueva (CsvProgressWebSocketHandlerTest): payload no-UUID no se registra; UUID válido sí; una segunda suscripción reemplaza a la primera (mapa de tamaño 1); el cierre elimina la suscripción.

* fix(security): remove PII from CSV progress messages

Problema: los mensajes de error por fila incrustaban el valor real del correo y la boleta, p. ej. "Row N: Email 'x@y.com' is already registered" y "Row N: Duplicate enrollment number '...' in CSV". Al viajar por el canal WebSocket (observable por un tercero que conozca el taskId), esto filtraba PII y actuaba como oráculo de "usuario existente confirmado" sobre la lista subida.

Vulnerabilidad mitigada: exposición de PII (correos/boletas) y confirmación de existencia de usuarios a través del canal de progreso.

Cambios (CsvRegistrationService.processRow): los cuatro mensajes que echoaban valores ahora referencian solo la fila y el campo, sin el dato:
- "Row N: Duplicate email in CSV"
- "Row N: Duplicate enrollment number in CSV"
- "Row N: Email is already registered"
- "Row N: Enrollment number is already registered"

El administrador que sube el archivo sigue teniendo el contexto (conoce la fila N de su propio CSV), pero el valor ya no viaja por el socket.

Efectos secundarios: ninguno funcional; solo cambia el texto de los mensajes de error (sin el valor). Las validaciones a nivel de campo ("email is invalid", "name is empty") ya no exponían valores y se conservan igual.

Prueba nueva (CsvRegistrationServiceTest): al procesar una fila connsajes de progreso contienen "already registered" pero no el correoni la boleta.

* fix(security): HUGE CHANGE!! invalidate outstanding JWTs on password change

Problema: resetPassword y updatePassword solo reescribían el hash. Los JWT son HS256 stateless (sesión STATELESS), así que un token robado seguía siendo válido hasta 24h después de que la víctima recuperara/cambiara su contraseña.

Vulnerabilidad mitigada: el flujo de "recuperar mi cuenta" no expulsaba al atacante que ya tenía un token.

Cambios:
- User: nuevo campo tokenVersion (int, @ColumnDefault("0") para que la columna se agregue de forma segura vía ddl-auto=update sobre filas existentes en Postgres).
- AuthService.generateToken: añade el claim tokenVersion con el valor actual del usuario.
- JwtUtil.extractTokenVersion: lee el claim (ausente → 0, para no invalidar en masa las sesiones al desplegar).
- JWTAuthenticationFilter: tras validar firma/expiración y usuario activo, rechaza el token si su tokenVersion no coincide con el del usuario.
- AuthService.updatePassword y RecoveryPasswordService.resetPassword: incrementan user.tokenVersion al persistir la nueva contraseña.

Efectos secundarios:
- Cambiar o resetear la contraseña invalida todos los JWT previos de ese usuario, incluida la sesión actual en /me/password (el usuario deberá reautenticarse). En el flujo de reset (no autenticado) no hay downside.
- Los tokens emitidos antes del despliegue no se invalidan en masa (claim ausente se trata como 0 = versión inicial); sólo dejan de valer cuando el usuario cambia su contraseña.

Pruebas nuevas:
- JwtUtilTest: extractTokenVersion con y sin claim.
- AuthServiceTest / RecoveryPasswordServiceTest: updatePassword y rnVersion.
- JWTAuthenticationFilterIntegrationTest: un token emitido antes del cambio deja de autenticar tras subir la versión (mientras uno nuevo sí).

* 🔧 fix(security): HUGE CHANGE!! store password-reset tokens hashed

Problema: PasswordResetToken.token guardaba el token en texto plano y era la clave de búsqueda; el correo llevaba ese mismo valor. La BD contenía el equivalente-contraseña en claro.

Vulnerabilidad mitigada: cualquier lectura de la BD (dump, réplica, backup, logs) entregaba tokens de toma de control directamente usables. Ahora la BD solo guarda un digest irreversible.

Cambios (RecoveryPasswordService):
- Al crear: se genera un token aleatorio, se persiste su hash SHA-256 y se envía el token crudo por correo.
- Al resetear: se hashea el token entrante y se busca por el hash (findByToken(hashToken(token))).
- Nuevo helper privado hashToken (SHA-256 → hex). La entidad PasswordResetToken no cambia (la columna ahora almacena el hash de 64 caracteres, cabe en el varchar(255) por defecto).

Efectos secundarios: los tokens de reset ya emitidos antes del despliegue dejan de ser válidos (la búsqueda es por hash). Dado que expiran a los 15 minutos y son de un solo uso, el impacto es nulo en la práctica.

Pruebas:
- RecoveryPasswordServiceTest: el token guardado es el SHA-256 del crudo emitido (no el crudo); resetPassword busca por el hash.
- Se actualizó RecoveryPasswordControllerIntegrationTest para reflejar el flujo real (guarda el hash en el setup, envía el crudo en la petición).

* fix(security): restrict CORS to configured frontend origins

Problema: SecurityConfig usaba addAllowedOriginPattern("*") junto con setAllowCredentials(true), lo que hacía que Spring reflejara cualquier Origin en Access-Control-Allow-Origin y emitiera Access-Control-Allow-Credentials: true, con CSRF deshabilitado.

Vulnerabilidad mitigada: eliminaba el aislamiento de origen del navegador — cualquier sitio podía hacer peticiones cross-origin con credenciales y leer las respuestas. Se volvería un problema de session-riding directo si se introdujeran cookies. Además era incoherente con la allowlist ya presente en el WebSocket.

Cambios (SecurityConfig): los orígenes ahora se leen de la propiedad frontend.url (mismo patrón que WebSocketConfig, split("\\s*,\\s*")) y se aplican con setAllowedOrigins(...) explícitos. Se conservan métodos, headers y credenciales.

Efectos secundarios:
- Producción debe tener FRONTEND_URL correctamente configurada (tu .env ya la fija en http://localhost:5173); admite varios orígenes separados por coma. Si no se configura, el valor resuelto es defaultFrontendUrl, que no coincide con ningún origen real → falla cerrado (bloquea CORS) en lugar de abrirse.

Prueba nueva (CorsConfigIntegrationTest): un preflight desde el origen configurado (http://localhost:3000 en el perfil test) recibe Access-Control-Allow-Origin; uno desde un origen no listado no lo recibe.

* fix(security): stop leaking internal exception details in responses

Problema: handleGenericException devolvía ex.getMessage() al cliente para cualquier excepción no mapeada, y handleHttpMessageNotReadable devolvía ex.getCause().getMessage(). Esto exponía nombres de tablas/columnas/constraints, fragmentos SQL, claves de objetos MinIO, mensajes NPE de Java 21, estructura de paquetes y valores de enums. Además, los errores genuinos de servidor no se registraban.

Vulnerabilidad mitigada: divulgación de información interna en las respuestas de error (500 y cuerpo JSON malformado).

Cambios (GlobalExceptionHandler):
- Se añadió un Logger; handleGenericException ahora registra la excepción completa (con traza) en el servidor y devuelve un mensaje genérico ("Internal server error") al cliente.
- handleHttpMessageNotReadable devuelve "Malformed request body" sin exponer la causa del parser.

Efectos secundarios: los clientes ya no reciben detalles internos en 500 ni en JSON malformado; la información de diagnóstico queda en los logs del servidor. Los demás handlers específicos (validación, credenciales, tokens, rate limit, etc.) no se tocaron, así que sus mensajes de negocio se conservan.

Prueba nueva (GlobalExceptionHandlerTest): el handler genérico devuelve 500 con "Internal server error" sin el detalle interno; el de cuerpo malformado devuelve "Malformed request body" sin los internos del parser.

* fix(security): fail fast on weak or missing JWT secret

 Problema: security.jwt.secret tenía un default committeado defaultSecret (débil y ni siquiera Base64 válido de 256 bits), y JwtUtil no validaba la longitud de la clave al arranque (fallaba tarde, en el primer uso del token).

Vulnerabilidad mitigada: un despliegue sin JWT_SECRET firmaría con una clave predecible/degradada → falsificación de tokens de autenticación. Ahora es imposible arrancar con una clave insegura.

Cambios:
- JwtUtil: nuevo @PostConstruct validateSecret() que decodifica el Base64 y exige ≥ 32 bytes (256 bits); lanza IllegalStateException si el secreto es ausente, no-Base64 o corto.
- application.properties: eliminado el default inseguro; security.jwt.secret=${JWT_SECRET:} (requerido, sin literal).

Efectos secundarios:
- ⚠️ JWT_SECRET debe estar configurada con un Base64 de ≥ 32 bytes; si no, el backend no arranca (fail-fast con mensaje claro). Tu .env ya tiene un JWT_SECRET de 43 caracteres (32 bytes), así que producción cumple.
- El perfil test ya define un secreto válido, por lo que todos los tests de integración arrancan correctamente.

Prueba nueva (JwtUtilTest): validateSecret rechaza secreto corto y en blanco, y acepta uno de ≥ 256 bits.

* fix(security): equalize login timing for unknown accounts

Problema: en AuthService.login, un usuario inexistente lanzaba la excepción sin ejecutar BCrypt, y un usuario inactivo saltaba BCrypt por el corto-circuito ||. Esto creaba un oráculo de tiempos que permitía distinguir {no existe} / {existe inactivo} / {existe activo} midiendo la latencia de la respuesta. Al aceptar boletas (estructuradas/secuenciales) como identificador, el espacio de enumeración es pequeño y recorrible.

Vulnerabilidad mitigada: enumeración de cuentas por tiempos en /api/auth/login.

Cambios (AuthService):
- Siempre se ejecuta una comparación BCrypt: si el usuario no existe, se compara contra un hash dummy precomputado (DUMMY_HASH); si existe, contra su hash real. Sólo después se decide el resultado (user == null || !isActive || !passwordMatches → credenciales inválidas).
- El mensaje uniforme ("Invalid credentials") se conserva.

Efectos secundarios: el coste de un login fallido por "usuario inexistente" o "inactivo" ahora es equivalente al de una contraseña incorrecta (una operación BCrypt), eliminando la señal de tiempo dominante. El residual (latencia de la consulta a BD) es despreciable frente a BCrypt.

Pruebas:
- Se actualizó login_WithNonExistentEmail_...: ahora verifica que sí se ejecuta matches(...) (antes afirmaba never()), y que no se emite token.
- Nueva login_WithInactiveUser_...: usuario inactivo lanza credenciales inválidas pero igualmente ejecuta la comparación.

* fix(security): send password-reset email asynchronously

Problema: MailSenderService.sendPasswordResetEmail era síncrono, a diferencia de sendWelcomeEmail. En el flujo /forgot, cuando el usuario existe se hacía una conexión/envío SMTP síncrono; cuando no existe se retornaba tras una consulta. Esa diferencia (típicamente cientos de ms) derrotaba el cuerpo de respuesta uniforme y permitía enumerar cuentas por tiempos.

Vulnerabilidad mitigada: enumeración de cuentas por tiempos en /api/recovery-password/forgot.

Cambios (MailSenderService): sendPasswordResetEmail ahora es @async("welcomeEmailExecutor") (mismo executor que el welcome) y envuelve su cuerpo en try/catch registrando fallos, igual que sendWelcomeEmail. La respuesta de /forgot ya no depende de la latencia SMTP.

Efectos secundarios:
- El envío del correo de reset ya no bloquea la respuesta; un fallo de SMTP se registra en logs y no interrumpe el flujo (el usuario puede volver a solicitar el reset, igual que con el welcome).
- El token se pasa como parámetro (no hay lectura de BD en el hilo async), así que no hay problema de commit/transacción.

Prueba nueva (MailSenderServiceAsyncTest): sendPasswordResetEmail ejecuta el envío en un hilo del executor (welcome-email-*), distinto del hilo llamante.

* fix(security): rate-limit authenticated password changes

 Problema: PUT /api/auth/me/password no tenía @ratelimit. Cada intento verifica currentPassword con un BCrypt completo, sin límite.

Vulnerabilidad mitigada: fuerza bruta del currentPassword con un JWT robado o una sesión de kiosco, y amplificación de CPU (1 petición = 1 BCrypt) usable como vector DoS.

Cambios (AuthController): se añadió @ratelimit(limit = 5, duration = 300, ...) al endpoint (5 intentos por 5 minutos, coherente con reset/signup) y la respuesta 429 documentada en OpenAPI. Reutiliza el aspecto de rate limiting ya endurecido en A1/A2 (clave endpoint | IP).

Efectos secundarios: ninguno para el uso normal; sólo se limita el abuso. La clave sigue siendo por IP+endpoint (el keying por principal quedaría para una mejora futura del aspecto, no requerida aquí).

Prueba nueva (AuthControllerRateLimitTest): verifica por reflexión que updatePassword está anotado con @ratelimit con limit/duration positivos. (El aspecto está deshabilitado bajo el perfil test, por lo que se valida la anotación en vez del 429 en vivo, consistente con el resto del proyecto.)

* fix(groups): enforce writability when removing students

 Problema: GroupService.removeStudent sólo llamaba a requireOwnedGroup, a diferencia de update/rotateJoinCode que además llaman a requireWritable. El propietario podía remover estudiantes de un grupo archivado o eliminado lógicamente, contra la regla documentada "archivado = solo lectura" (llms/backend/groups/README.md, ciclo de vida de grupos).

Riesgo mitigado: desviación de la invariante de solo-lectura (es un bug de regla de negocio; owner-only, no un salto de control de acceso). Alinea el comportamiento con la documentación.

Cambios (GroupService.removeStudent): se añadió requireWritable(group) tras la comprobación de propiedad. requireWritable ya cubre ambos casos (eliminado lógico → "Group is deleted"; archivado → "Group is archived and read-only"), devolviendo 400.

Efectos secundarios: remover un estudiante de un grupo archivado/eliminado ahora responde 400 en vez de aplicar el cambio; en grupos activos y no archivados funciona igual que antes.

Pruebas actualizadas/nuevas:
- GroupServiceTest: se reemplazó el test que documentaba la conductjectsArchivedAndDeletedGroups (rechaza ambos) yremoveStudentSucceedsOnActiveWritableGroup (camino feliz).
- GroupControllerIntegrationTest: archivedGroupRejectsChangesInclud 400 en la remoción y que la inscripción permanezca ACTIVE.

* fix(security): add explicit scope checks to admin user mutations

Problema: update, deactivate y restore en AdminUserController no tenían @PreAuthorize a nivel de método; sólo aplicaba la anotación de clase hasAuthority('ADMIN'). El gate de scope real vivía únicamente en AdminUserService.requireMutationPermission. Una regresión en el servicio degradaría estas operaciones silenciosamente a "cualquier admin".

Riesgo mitigado (defensa en profundidad): se añade una segunda barrera declarativa, de modo que un admin sin ningún scope de gestión de usuarios no alcanza el endpoint aunque el chequeo de servicio fallara.

Cambios (AdminUserController):
- update (PATCH): hasAuthority('ADMIN') and hasAnyAuthority('UPDATE_USERS', 'UPDATE_ADMINS').
- deactivate (DELETE) y restore (POST /restore): hasAuthority('ADMIN') and hasAnyAuthority('MANAGE_USER_STATUS', 'MANAGE_ADMIN_STATUS').

Son gates gruesos (por eso hasAnyAuthority): el scope preciso según el rol del objetivo (usuario vs admin), la protección de auto-gestión y la del último superadmin siguen decidiéndose en el servicio. SUPER_ADMIN expande a todos los scopes, así que no se ve afectado.

Efectos secundarios: ninguno en el camino de aceptación (quien ya pasaba el servicio tiene los scopes requeridos). Un admin con ADMIN pero sin ningún scope relevante ahora recibe 403 en la anotación en vez de en el servicio — mismo código de estado.

Prueba nueva (AdminUserControllerIntegrationTest): un admin con MANAGE_USER_STATUS/MANAGE_SCOPES pero sin UPDATE_* recibe 403 al intentar PATCH de actualización.

* feat(metrics): expose per-group self metrics to students

 Qué se agregó: nuevo endpoint de auto-consulta para que un estudiante vea su propio desempeño agregado en un grupo.

Contrato nuevo:
- GET /api/groups/{groupId}/metrics/me → SuccessResponse<StudentMetricsDTO> (mis agregados: publishedAssignments, submittedCount, completionRate, lateCount, averageScore, gradedAssignments, totalAttempts, missingAssignmentIds, + mi identidad).
- 200 inscrito · 403 no inscrito · 404 grupo inexistente/eliminado · 401 sin JWT.

Cambios:
- GroupMetricsService: nuevo myGroupMetrics(groupId, email) que reutiliza loadGroupSnapshot, publishedAssignments, percent, averageNormalizedScore, fullName, y un gate nuevo requireActiveEnrollment(...) (usuario del JWT + inscripción ACTIVE + grupo isActive, si no 403/404). Calcula la fila del alumno autenticado igual que el cálculo docente, filtrando calificaciones a solo RETURNED (los borradores no cuentan en averageScore ni gradedAssignments). Nuevo record interno StudentContext.
- StudentMetricsController (nuevo, @tag "Student Metrics"), método me(...) con @PreAuthorize("hasAuthority('STUDENT')"), separado del controlador docente.

Diseño clave (diferencias con el panel docente): autorización por inscripción activa (no propiedad); solo datos propios; calificaciones solo devueltas. Se reutiliza el DTO StudentMetricsDTO existente tal cual.

Efectos secundarios: ninguno sobre las métricas docentes; solo se añade superficie nueva de lectura.

Prueba nueva (StudentMetricsControllerIntegrationTest, 5 casos): alumno inscrito con entrega/AC/calificación devuelta ve sus agregados; alumno sin entrega → ceros y averageScore null; calificación en borrador oculta (score null, graded 0); alumno no inscrito → 403; grupo eliminado → 404.

* feat(metrics): expose per-assignment self metrics to students

Contrato nuevo:
- GET /api/groups/{groupId}/metrics/me/assignments → SuccessResponse<List<StudentAssignmentMetricsDTO>>, una fila por tarea publicada del grupo, ordenada por createdAt desc.
- 200 inscrito · 403 no inscrito · 404 grupo inexistente/eliminado · 401 sin JWT.

Cambios:
- Nuevo DTO StudentAssignmentMetricsDTO (model/dto/metrics/): assignmentId, title, dueDate, closeDate, maxPoints, workStatus, currentSubmissionId, deliveredLate, attempts, verdict, timeMs, memoryMb, grade (reutiliza AssignmentMetricsDetailDTO.GradeSummary, solo si RETURNED).
- GroupMetricsService.myAssignmentMetrics(groupId, email): mismo gate requireActiveEnrollment de S1; reutiliza el GroupSnapshot (sin consultas nuevas — las filas de proyección ya traen assignmentId/workStatus), arma la fila del alumno autenticado por cada tarea publicada, con calificaciones solo RETURNED.
- StudentMetricsController: método myAssignments(...) con @PreAuthorize("hasAuthority('STUDENT')").

Nota de diseño: el workStatus proviene de CurrentSubmissionRow (preciso para SUBMITTED/RETURNED); una tarea sin entrega vigente reporta NOT_SUBMITTED con submission/veredicto nulos, aunque attempts puede ser > 0 si hubo entregas retiradas/reemplazadas (M7 cuenta todas). Consistente con el resumen S1.

Efectos secundarios: ninguno sobre las métricas docentes; solo superficie de lectura nueva.

Pruebas nuevas (StudentMetricsControllerIntegrationTest, +3): desglose del alumno con entrega (workStatus SUBMITTED, verdict AC, time/memory, grade 95/RETURNED); alumno sin entrega (fila vacía, grade null, attempts 0); calificación en borrador oculta; y 403 para no inscrito en ambos endpoints.

* docs(metrics): document student-facing self metrics

---------

Co-authored-by: JohannTF <trejofloresjohanndaniel@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 20, 2026 05:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

…onversion in compound assignment'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 25, 2026 04:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Comment on lines +64 to +65
webSocketHandler.queueTask(taskId, requester.getId(),
() -> selfProvider.getObject().processAsync(csvData, taskId));
Copilot AI balanced review requested due to automatic review settings October 29, 2026 05:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Copilot AI balanced review requested due to automatic review settings November 1, 2026 01:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical issues allow CSV registration after permission revocation, expose WebSocket tickets in logs, and let stale password-change responses restore or replace sessions.

16 open issues
22 resolved since last review

And 2 more resolved.

Review effort: Balanced

Comment on lines +64 to +65
webSocketHandler.queueTask(taskId, requester.getId(),
() -> selfProvider.getObject().processAsync(csvData, taskId));
logging.level.org.hibernate.orm.jdbc.bind=WARN

# Log Spring MVC requests and completion status to the application console.
logging.level.org.springframework.web.servlet.DispatcherServlet=DEBUG
Comment on lines +35 to +38
async updatePassword(currentPassword: string, newPassword: string) {
const response = await authApi.updatePassword(currentPassword, newPassword, getAuthToken());
setAuthToken(response.data.token);
return response;
Comment on lines +17 to +22
const raw = window.localStorage.getItem(draftKey(userId, assignmentId));
if (!raw) return null;
const value: unknown = JSON.parse(raw);
if (!value || typeof value !== "object") return null;
const draft = value as Partial<CodeDraft>;
if (typeof draft.language !== "string" || !draft.code || typeof draft.code !== "object") return null;
@@ -1,22 +1,4 @@
import { API_BASE_URL } from "~/core/config/env";
import { getAuthToken } from "~/core/storage/token.storage";
import { AuthService } from "~/features/auth/services/auth.service";
{/* Subtitle */}
<p className="text-lg lg:text-xl text-gray-600 dark:text-gray-400 max-w-xl mx-auto lg:mx-0">
Reduce your grading time with autograded assignments, designed to engage students and provide instant feedback.
Reduce your grading time, this plaform was designed to engage students and provide instant feedback.
Comment thread llms/backend/OVERVIEW.md
Comment on lines +47 to +48
- `postgresAssistantTest` checks assistant concurrency and development-schema indexes/constraints against an explicitly configured disposable PostgreSQL database. `liveAssistantTest` is an opt-in synthetic Gemini smoke, excluded from normal CI. See `docs/AI_ASSISTANT_ROLLOUT.md`.
- Contract and PostgreSQL development schema checklist live in `docs/AI_ASSISTANT_STAGE_0_CONTRACT.md` and `docs/AI_ASSISTANT_DEV_SCHEMA.sql`.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 02:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor


while read -r image; do
if docker pull --quiet "$registry/sandbox/$image"; then
docker tag "$registry/sandbox/$image" "$image"
COPY --from=build /workspace/build/libs/*-SNAPSHOT.jar app.jar
# Entities store LocalDateTime and the browser reads those values as local time.
ENV TZ=America/Mexico_City
USER 10001
Copilot AI balanced review requested due to automatic review settings November 2, 2026 16:32
…n menu

- Replace the fixed icon rail with a hamburger drawer below 640px for the
  student, teacher and admin shells; one nav list feeds both the rail and
  the drawer, and the breadcrumb collapses to the current page on phones.
- Student dashboard: stack the groups/submissions aside under the main
  column below lg instead of squeezing it to 48px.
- Workspace header: keep Run sample and Resubmit on screen at tablet width.
- Fix horizontal overflow on the teacher dashboard, CSV upload, join code
  inputs and the reschedule date fields.
- Loosen assignment and grade rows and the group detail actions on phones.
…ng hero

- Extract the "How it works" hexagon pattern into a shared
  HexGridBackground and use it instead of the square grid on the landing
  hero and the password reset panels.
- Add a blinking editor cursor after the last line of the hero code sample
  (static when reduced motion is preferred).
- New shared AuthSidePanel for the brand panel: eyebrow, headline, code
  card and feature list, with facts that match the backend (15-minute
  single-use reset link, 3 requests / 5 min, Java/Python/C/C++).
- Restyle the sign-in form: "Welcome to CodeHive", envelope and prompt
  icons, "Forgot?" next to the password label, Continue button and a short
  Terms/Privacy note.
- Remove the "Remember me for 30 days" checkbox, which was never sent to
  the backend.
- Recovery confirmation: replace the incorrect "same browser session"
  notice and the dead support link with accurate guidance.
- Add a `short` variant (max-height 820px) so the brand panels fit
  laptop-height windows instead of cutting off their footer.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Copilot AI balanced review requested due to automatic review settings November 2, 2026 16:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

@JohannTF JohannTF changed the title Feature/ai assistant Add AI student assistant, AI usage analytics and secure CSV progress Nov 8, 2026
@JohannTF
JohannTF merged commit 9ce193b into main Nov 10, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants