diff --git a/services/lms-service/src/handlers.rs b/services/lms-service/src/handlers.rs index 04224c5..8078c53 100644 --- a/services/lms-service/src/handlers.rs +++ b/services/lms-service/src/handlers.rs @@ -4884,7 +4884,16 @@ pub async fn get_lesson_collaborative_doc( .bind(org_ctx.id) .fetch_optional(&pool) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|e| { + tracing::error!( + "get_lesson_collaborative_doc: failed to fetch doc for lesson {} org {}: {}", + id, + org_ctx.id, + e + ); + StatusCode::INTERNAL_SERVER_ERROR + }) + .unwrap_or(None); let doc = row.unwrap_or(DocRow { content: String::new(), @@ -4967,9 +4976,16 @@ pub async fn update_lesson_collaborative_doc( if existing.is_none() && payload.base_revision == 0 { // Primer guardado let course_id = sqlx::query_scalar::<_, Uuid>( - "SELECT course_id FROM lessons WHERE id = $1", + r#" + SELECT m.course_id + FROM lessons l + JOIN modules m ON m.id = l.module_id + JOIN courses c ON c.id = m.course_id + WHERE l.id = $1 AND c.organization_id = $2 + "#, ) .bind(id) + .bind(org_ctx.id) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; diff --git a/services/lms-service/src/main.rs b/services/lms-service/src/main.rs index 5703e1e..b8e0a94 100644 --- a/services/lms-service/src/main.rs +++ b/services/lms-service/src/main.rs @@ -144,8 +144,10 @@ async fn main() { use std::sync::Arc; let mut governor_conf = GovernorConfigBuilder::default() - .const_per_second(10) - .const_burst_size(50) + // La vista de lecciones abre varias solicitudes concurrentes + 2 streams SSE. + // Con lĂ­mites muy bajos se generan 429 al navegar entre lecciones. + .const_per_second(80) + .const_burst_size(240) .key_extractor(SmartIpKeyExtractor); let governor_conf = Arc::new(governor_conf.finish().unwrap()); diff --git a/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx b/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx index 5faca9d..003df07 100644 --- a/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx +++ b/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx @@ -44,6 +44,7 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les const [allGrades, setAllGrades] = useState([]); const [isBookmarked, setIsBookmarked] = useState(false); const { user } = useAuth(); + const userId = user?.id ?? null; useEffect(() => { const fetchAll = async () => { @@ -55,9 +56,9 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les setLesson(lessonData); setCourse({ ...outlineData.course, modules: outlineData.modules }); - if (user) { + if (userId) { const [grades, bookmarks] = await Promise.all([ - lmsApi.getUserGrades(user.id, params.id), + lmsApi.getUserGrades(userId, params.id), lmsApi.getBookmarks(params.id) ]); setAllGrades(grades); @@ -72,7 +73,7 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les } }; fetchAll(); - }, [params.id, params.lessonId, user]); + }, [params.id, params.lessonId, userId]); useEffect(() => { if (typeof window === "undefined") return; diff --git a/web/experience/src/components/CollaborativeDocEditor.tsx b/web/experience/src/components/CollaborativeDocEditor.tsx index 93d22be..180f1c3 100644 --- a/web/experience/src/components/CollaborativeDocEditor.tsx +++ b/web/experience/src/components/CollaborativeDocEditor.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { lmsApi, getLmsApiUrl, CollaborativeDoc, UpdateCollaborativeDocResponse } from "@/lib/api"; +import { lmsApi, getLmsApiUrl, getToken, CollaborativeDoc, UpdateCollaborativeDocResponse } from "@/lib/api"; import { AlertTriangle, CheckCircle, @@ -50,11 +50,9 @@ export default function CollaborativeDocEditor({ lessonId }: Props) { // SSE: escuchar cambios remotos useEffect(() => { - const token = typeof window !== "undefined" - ? localStorage.getItem("lms_token") ?? sessionStorage.getItem("lms_token") ?? "" - : ""; + const token = getToken() || ""; const baseUrl = getLmsApiUrl(); - const url = `${baseUrl}/lessons/${lessonId}/collaborative-doc/stream?preview_token=${encodeURIComponent(token)}`; + const url = `${baseUrl}/lessons/${lessonId}/collaborative-doc/stream${token ? `?preview_token=${encodeURIComponent(token)}` : ""}`; const es = new EventSource(url); sseRef.current = es; diff --git a/web/experience/src/components/CollaborativeWhiteboard.tsx b/web/experience/src/components/CollaborativeWhiteboard.tsx index cb38705..bcc500b 100644 --- a/web/experience/src/components/CollaborativeWhiteboard.tsx +++ b/web/experience/src/components/CollaborativeWhiteboard.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent } from "react"; -import { lmsApi, getLmsApiUrl, CollaborativeCanvasState } from "@/lib/api"; +import { lmsApi, getLmsApiUrl, getToken, CollaborativeCanvasState } from "@/lib/api"; import { AlertTriangle, CheckCircle, Loader2, RefreshCw, Save, Trash2 } from "lucide-react"; type ConflictInfo = { @@ -61,6 +61,16 @@ export default function CollaborativeWhiteboard({ lessonId }: Props) { const [conflict, setConflict] = useState(null); const isDrawing = useRef(false); + const dirtyRef = useRef(false); + const revisionRef = useRef(0); + + useEffect(() => { + dirtyRef.current = dirty; + }, [dirty]); + + useEffect(() => { + revisionRef.current = revision; + }, [revision]); const allStrokes = useMemo(() => { return draftStroke ? [...strokes, draftStroke] : strokes; @@ -219,24 +229,20 @@ export default function CollaborativeWhiteboard({ lessonId }: Props) { useEffect(() => { const base = getLmsApiUrl(); - // getToken no es exportada; leemos directamente de sessionStorage/localStorage - const token = - (typeof window !== "undefined" && - (sessionStorage.getItem("preview_token") || localStorage.getItem("experience_token"))) || - ""; + const token = getToken() || ""; const url = `${base}/lessons/${lessonId}/collaborative-canvas/stream${token ? `?preview_token=${encodeURIComponent(token)}` : ""}`; const es = new EventSource(url); es.onmessage = (ev) => { - if (dirty || isDrawing.current) return; + if (dirtyRef.current || isDrawing.current) return; try { const data = JSON.parse(ev.data as string) as { revision: number; canvas_state: CollaborativeCanvasState; updated_at: string; }; - if (data.revision !== revision) { + if (data.revision !== revisionRef.current) { setStrokes(toStrokeArray(data.canvas_state || DEFAULT_CANVAS)); setRevision(data.revision); setLastSavedAt(data.updated_at); @@ -253,7 +259,7 @@ export default function CollaborativeWhiteboard({ lessonId }: Props) { return () => { es.close(); }; - }, [dirty, lessonId, revision]); + }, [lessonId]); return (
diff --git a/web/experience/src/lib/api.ts b/web/experience/src/lib/api.ts index 0005301..72962ae 100644 --- a/web/experience/src/lib/api.ts +++ b/web/experience/src/lib/api.ts @@ -586,7 +586,7 @@ export interface UpdateAnnouncementPayload { -const getToken = () => { +export const getToken = () => { if (typeof window === 'undefined') return null; // Check for preview token in URL