feat(parent): implement complete Parent Portal#273
Conversation
- Parent dashboard with real supports from DB, status management - 6-step support creation form with email-based child linking - AI chat sessions within parent space (no redirect to student) - Auto-linking chat_id to support via POST /chats/new for parent role - AI-generated QCM evaluations using Ollama gemma3:4b (2-step generation) - Real sessions view with metrics calculated from chat messages - Dynamic progress tracking updated every 15s and on each AI response
|
Hi, @WissaalElakebii Security review — Parent portal featureThe feature direction is solid, but several issues must be fixed before this can ship to users 🔴 Critical (2) — Must fix before merge🔴 [CRITICAL — Blocking] Multiple frontend files — hardcoded The string
This hardcoded UUID is a real student's ID committed to the public repository. Any visitor can use it to query Required before merge:
🔴 [CRITICAL — Blocking]
For minors, GDPR (Art. 8) and equivalent regulations require that children's personal data — including learning conversations — is processed with appropriate safeguards. Exposing a teenager's private AI tutoring session to a parent without any consent mechanism or age-based policy is a legal and ethical risk. Required before merge:
🟠 High (5)🟠 [HIGH]
Fix:
🟠 [HIGH] The upload endpoint verifies the parent↔student link via Fix: Before calling 🟠 [HIGH] The new code in Fix: For parents, also verify via 🟠 [HIGH] The dashboard response contains fabricated KPIs ( This is a product integrity issue with potential legal implications (misleading parents about their child's actual performance). Required before merge: Either compute real metrics from the database or clearly label the dashboard as "demo mode". Do not ship fake data as if it were real. 🟠 [HIGH]
Fix: Persist notifications in a 🟡 Medium (4)🟡 [MEDIUM] QCM question text, answer choices, and explanations come directly from the Ollama LLM and are rendered in the DOM via Svelte's default text interpolation (safe by default). However, If any developer adds Fix: Add a lint rule forbidding 🟡 [MEDIUM]
A student who has 10 short back-and-forth messages gets their support marked complete, even if they haven't finished learning. This is a write operation on a Fix: Remove the status write from the 🟡 [MEDIUM] The "Notification preferences" section renders toggle switches that look interactive but have no Fix: Either wire the toggles to a real preferences API, or remove the section entirely until it's implemented. Shipping fake interactive controls erodes user trust. 🟡 [MEDIUM — GDPR] Frontend parent pages — Multiple
In production, these are readable by browser extensions and visible during screen sharing. Fix: Remove or gate all |
|
Hi @WissaalElakebii
Could you complete both before this is ready for review? |
baaki-hicham
left a comment
There was a problem hiding this comment.
Hi, thank you for your contribution.
Multiple blocking issues found during local testing
Tested locally with a fresh parent account. Screenshots attached.
1. Hardcoded student ID — entire portal broken for any real account
Found in both ParentEvaluations.svelte and parent/c/[id]/+page.svelte:
const studentId = localStorage.getItem('parent_student_id')
?? 'e7081ab6-fce1-4111-ae63-74c0e6ae46b6';This UUID only exists in the author's local database. For any other parent account, assert_owns_student correctly rejects it → 403 Forbidden on every page that depends on studentId (Evaluations, AI Sessions, Dashboard, chat progress). Confirmed in the Network tab:
GET /api/v1/parent/evaluations/by-student/e7081ab6-... → 403 Forbidden
GET /api/v1/parent/sessions-real/e7081ab6-... → 403 Forbidden
Suggestion: use the /parent/supports/find-student endpoint (already present in parent_supports.py) to resolve the linked student from the authenticated parent's account, instead of a hardcoded fallback.
2. Generated prompt is never passed to the chat
ParentSupportCreation.svelte builds a detailed prompt via generatePrompt() (subject, level, tutor style, learning objective…), but this prompt is never transmitted to <Chat />. The chat page instantiates the component with no context:
<Chat on:chatEvent={handleChatEvent} />As a result, the AI starts from scratch and ignores everything the parent filled in the form — the core value proposition of this feature is broken.
Suggestion: pass the generated prompt as a prop to <Chat /> (e.g. initialPrompt or systemPrompt), or store it in a Svelte store/localStorage before navigating, and consume it on mount in the chat page.
3. Wrong redirect after support creation
After form submission, the code does:
goto('/student/supports');This sends the parent into the student interface instead of /parent/chat. The parent loses the context of what they just created.
4. Fragile URL-polling hack in parent/c/[id]/+page.svelte
interceptInterval = setInterval(() => {
if (path.startsWith('/student/c/')) {
goto(`/parent/c/${id}`, { replaceState: true });
}
}, 100);
setTimeout(() => clearInterval(interceptInterval), 30000);Polling window.location.pathname every 100ms for 30 seconds to intercept a redirect is a fragile workaround. It indicates that <Chat /> internally forces navigation to /student/c/ after chat creation, and that the parent portal hasn't fixed this at the source. This should be addressed by either patching <Chat /> to accept a redirectTarget prop, or by not reusing the student <Chat /> component directly in the parent portal.
5. Branding inconsistency (non-blocking)
The sidebar displays a plain "OT" text badge instead of the actual Open TutorAI logo. Per the frontend charter, UI components must stay consistent with the project's established visual identity and not use placeholder branding.
6. Frontend design does not comply with the project's frontend charter
The entire Parent Portal uses inline styles exclusively (style="...") instead of Tailwind CSS utility classes, which are the standard throughout the rest of the codebase. This introduces several violations of the project's frontend charter.
a) Hardcoded hex colors outside the official palette
The project's design system is built on a single gray-50 → gray-950 scale defined in tailwind.config.js. The Parent Portal introduces at least 11 new arbitrary colors without a team design decision:
#2563EB (blue) → buttons, card borders, spinner, active filters
#16A34A (green) → "Actifs" stat card
#D97706 (orange) → "En attente" stat card
#7C3AED (violet) → "Terminés" stat card
#111827, #6B7280, #E5E7EB, #EFF6FF, #D1FAE5, #DBEAFE, #FEF3C7
Per the frontend charter:
"Introducing a new color outside the established gray palette requires a team design decision — it should not be done unilaterally in a component."
b) No Tailwind classes used
Every style in ParentDashboard.svelte and ParentLayout.svelte is written as inline style="...". This bypasses ESLint/Prettier checks, makes the code harder to maintain, and breaks consistency with the rest of the project where Tailwind utility classes are the standard.
c) No dark mode support
Since all styles are inline, there is no dark: variant anywhere in the Parent Portal. Switching to dark mode in the app will leave the entire portal with a broken white background and unreadable text, while the rest of the application adapts correctly.
d) Logo inconsistency
The sidebar shows a plain OT text badge (blue circle) instead of the actual Open TutorAI logo used elsewhere in the application. This creates a visual inconsistency between the Parent Portal and the rest of the product.
Suggestion: rewrite the Parent Portal components using Tailwind utility classes from the existing gray-* palette, add dark: variants, use the real logo asset, and open a separate discussion if new brand colors (e.g. a primary blue) are needed so the team can decide and add them to tailwind.config.js properly.
Summary: Issues 1–4 are blocking — the Parent Portal does not
function for any real account outside the author's local setup.
Issues 5–6 are non-blocking but should be addressed before merge
to stay consistent with the project's standards. Happy to help
review a follow-up once these are fixed! 🙌


There was a problem hiding this comment.
Pull request overview
Implements a new “Parent Portal” vertical across the SvelteKit UI and FastAPI backend, adding parent-facing pages (dashboard, supports, AI sessions, notifications, evaluations) plus new API routes/models to support parent↔student workflows and evaluation generation.
Changes:
- Added Parent Portal UI routes/layout and parent-specific pages/components for supports, sessions, notifications, evaluations, and chat.
- Introduced new backend routers for parent supports, dashboard/notifications/sessions/progress, and AI-generated evaluations (with a new
EvaluationORM model). - Added parent↔student linking service/model and integrated parent routers into app registration.
Reviewed changes
Copilot reviewed 29 out of 30 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/routes/parent/support/create/+page.svelte | Route wrapper for parent support creation UI |
| ui/src/routes/parent/support/[id]/+page.svelte | Parent support detail page (fetch + display) |
| ui/src/routes/parent/sessions/+page.svelte | Route wrapper for parent sessions page |
| ui/src/routes/parent/notifications/+page.svelte | Route wrapper for parent notifications page |
| ui/src/routes/parent/evaluations/+page.svelte | Route wrapper for parent evaluations page |
| ui/src/routes/parent/evaluation/[id]/+page.svelte | Parent evaluation detail/quiz + submission UI |
| ui/src/routes/parent/dashboard/+page.svelte | Route wrapper for parent dashboard page |
| ui/src/routes/parent/chat/+page.svelte | Parent chat launcher + chatCreated handling |
| ui/src/routes/parent/c/[id]/+page.svelte | Parent chat view with support progress polling |
| ui/src/routes/parent/+page.svelte | Parent landing page formatting tweaks |
| ui/src/routes/parent/+layout.svelte | Parent portal layout (sidebar/header) + model loading |
| ui/src/lib/components/student/tutor/Chat.svelte | Minor change (spacing) |
| ui/src/lib/components/parent/pages/ParentSessions.svelte | Parent sessions UI fed by /parent/sessions-real |
| ui/src/lib/components/parent/pages/ParentNotifications.svelte | Parent notifications UI + mark-as-read actions |
| ui/src/lib/components/parent/pages/ParentEvaluations.svelte | Parent evaluations list + generate-eval modal/workflow |
| ui/src/lib/components/parent/pages/ParentDashboard.svelte | Parent supports dashboard + start/view AI session actions |
| ui/src/lib/components/parent/elements/ParentSupportCreation.svelte | Multi-step parent support creation form + uploads |
| ui/src/lib/components/parent/elements/ParentLayout.svelte | Additional parent layout component (separate from route layout) |
| ui/src/lib/apis/parent/index.ts | Added parent API client helpers (supports/notifications/link-chat/etc.) |
| learning/supports/service.py | Sets support status active when chat_id is linked |
| gateway/http/routers/parent_supports.py | New parent supports router (create/list/detail/upload/link-chat/find-student) |
| gateway/http/routers/parent_evaluations.py | New evaluations router (generate/list/get/submit) + Ollama calls |
| gateway/http/routers/parent_dashboard.py | New parent dashboard/notifications/sessions/progress endpoints |
| gateway/http/routers/chats.py | Auto-link chat to support when support_id is provided |
| gateway/http/app.py | Registers new parent routers under /api/v1 |
| data/models/evaluation.py | New Evaluation ORM model |
| data/models/init.py | Registers ParentStudentLink and Evaluation models |
| accounts/parents/service.py | New ParentService for parent↔student linkage and support creation delegation |
| accounts/parents/models.py | New ParentStudentLink ORM model |
| accounts/parents/init.py | Package init |
| const res = await fetch( | ||
| `http://localhost:8080/api/v1/parent/supports/find-student?email=${encodeURIComponent(studentEmail.trim())}`, | ||
| { headers: { authorization: `Bearer ${token}` } } | ||
| ); |
| $: canProceed = | ||
| currentStep === 0 ? true : | ||
| currentStep === 1 ? supportTitle.trim().length > 0 && (!!selectedSubject || customSubject.trim().length > 0) : | ||
| currentStep === 2 ? true : |
| toast.success('Soutien créé avec succès ! 🎉'); | ||
| goto('/student/supports'); | ||
| } catch (err: any) { |
| // Utiliser l'ID étudiant depuis localStorage | ||
| const studentId = localStorage.getItem('parent_student_id') | ||
| ?? 'e7081ab6-fce1-4111-ae63-74c0e6ae46b6'; | ||
|
|
| const token = localStorage.getItem('token'); | ||
| if (!token) { goto('/auth'); return; } | ||
| studentId = localStorage.getItem('parent_student_id') ?? 'e7081ab6-fce1-4111-ae63-74c0e6ae46b6'; | ||
| studentName = localStorage.getItem('parent_student_name') ?? ''; |
| async def generate_qcm_with_ai(chat_content: str, subject: str, title: str) -> List[Dict]: | ||
| """Génère 10 QCM en 2 étapes pour garantir la qualité.""" | ||
|
|
||
| ollama_url = "http://localhost:11434" | ||
|
|
||
| # ── ÉTAPE 1 : Extraire les faits clés de la conversation ────────────────── | ||
| prompt_extract = f"""Lis cette conversation entre un élève et un tuteur IA sur le sujet: {subject} / {title} | ||
|
|
||
| CONVERSATION: | ||
| {chat_content[:3500]} | ||
|
|
||
| Liste les 10 points/faits importants appris dans cette conversation. | ||
| Format: une ligne par point, numéroté de 1 à 10. | ||
| Ne mets que les faits, pas de commentaires.""" | ||
|
|
||
| facts = [] | ||
| try: | ||
| async with httpx.AsyncClient(timeout=120.0) as client: | ||
| r = await client.post( | ||
| f"{ollama_url}/api/generate", | ||
| json={"model": "gemma3:4b", "prompt": prompt_extract, "stream": False, | ||
| "options": {"temperature": 0.2, "num_predict": 800}} | ||
| ) | ||
| r.raise_for_status() | ||
| facts_text = r.json().get("response", "") | ||
| print(f"[QCM] Faits extraits: {facts_text[:200]}") | ||
| # Parser les faits numérotés | ||
| for line in facts_text.strip().split("\n"): | ||
| line = line.strip() | ||
| if line and (line[0].isdigit() or line.startswith("-")): | ||
| fact = line.lstrip("0123456789.-) ").strip() | ||
| if len(fact) > 10: | ||
| facts.append(fact) | ||
| except Exception as e: | ||
| print(f"[QCM] Erreur extraction faits: {e}") | ||
|
|
||
| if not facts: | ||
| # Utiliser directement le contenu de la conversation comme base | ||
| facts = [chat_content[i:i+200] for i in range(0, min(len(chat_content), 2000), 200)] | ||
|
|
||
| # ── ÉTAPE 2 : Générer un QCM par fait ───────────────────────────────────── | ||
| questions = [] | ||
| facts_to_use = facts[:10] | ||
|
|
||
| for i, fact in enumerate(facts_to_use): | ||
| prompt_qcm = f"""Sur la base de ce fait appris en cours de {subject}: | ||
| "{fact}" | ||
|
|
||
| Génère UNE question QCM avec 4 choix (A, B, C, D). | ||
| Réponds UNIQUEMENT avec ce format JSON exact, une seule ligne: | ||
| {{"question": "...", "A": "...", "B": "...", "C": "...", "D": "...", "correct": "A", "explanation": "..."}}""" | ||
|
|
||
| try: | ||
| async with httpx.AsyncClient(timeout=60.0) as client: | ||
| r = await client.post( | ||
| f"{ollama_url}/api/generate", | ||
| json={"model": "gemma3:4b", "prompt": prompt_qcm, "stream": False, | ||
| "options": {"temperature": 0.3, "num_predict": 300}} | ||
| ) | ||
| r.raise_for_status() |
| @router.post("/create", response_model=ParentSupportResponse) | ||
| async def create_support_for_child( | ||
| data: ParentSupportCreateRequest, | ||
| current_user: User = Depends(get_current_user), | ||
| svc: SupportsService = Depends(get_supports_service), | ||
| db: Session = Depends(get_db), | ||
| ): |
| @router.post("/generate") | ||
| async def generate_evaluation( | ||
| data: GenerateEvalRequest, | ||
| current_user: User = Depends(get_current_user), | ||
| db: Session = Depends(get_db), | ||
| ): | ||
| """Génère un QCM de 10 questions basé sur une session de chat.""" | ||
| _require_parent(current_user) | ||
|
|
||
| # Vérifier liaison parent-enfant | ||
| try: | ||
| ParentService(db).assert_owns_student(current_user.id, data.student_id) | ||
| except AuthorizationError as e: | ||
| raise HTTPException(status_code=403, detail=str(e)) | ||
|
|
| # Lier automatiquement le chat au soutien si support_id fourni | ||
| support_id = body.chat.get("support_id") | ||
| if support_id: | ||
| try: | ||
| from data.models import Support | ||
| from datetime import datetime | ||
| support = db.query(Support).filter(Support.id == support_id).first() | ||
| # Le soutien appartient a l'etudiant mais le parent peut aussi le lier | ||
| if support and (support.user_id == current_user.id or current_user.role in ("parent", "admin")): | ||
| support.chat_id = chat.id | ||
| support.status = "active" | ||
| support.updated_at = datetime.utcnow() | ||
| db.commit() | ||
| print(f"Chat {chat.id} lie au soutien {support_id}") | ||
| except Exception as e: | ||
| print(f"Support link error: {e}") |
| contents = await file.read() | ||
| if len(contents) > max_bytes: | ||
| raise _too_large |
Summary
This PR implements the complete Parent Portal.
Features