Unifies the new LangGraph-driven course-plan/media flow with robust provider fallbacks, admin AI provider settings, editable book-style materials, and strict entity isolation across LMS/course-plan APIs. Adds admin-only entity membership management in the Entities UI so users can switch linked entities directly from the platform.
Made-with: Cursor
Builds the §24 product on top of the LangGraph runtime from §22:
Phase A (Sources / RAG)
- encoach.course.plan.source model (file | url | text)
- SourceIndexer extracts PDF (pypdf), DOCX (python-docx), HTML, plain
text and embeds chunks via the existing pgvector pipeline scoped to
plan_id, so resources.search only returns the plan's own corpus
- Endpoints: list/create/upload/reindex/delete + plan-scoped retrieval
Phase B (Deliverables)
- services.deliverables.compute_deliverables walks the plan, derives
{planned, generated, ready} per week from material + media state
- GET /api/ai/course-plan/<id>/deliverables drives the new wizard
preview step and the live progress strip on the detail page
Phase C (Multi-modal media)
- encoach.course.plan.media model + MediaService:
audio: AWS Polly (default) or ElevenLabs
image: OpenAI DALL-E 3, capped per plan via system parameter
video: local ffmpeg subprocess (image + audio -> MP4 1280x720)
- Three new agent tools (media.synthesize_audio / generate_image /
compose_video), wired into course_week_materials and a new
course_media_director agent
- Endpoints per material + week-level batch generator
Phase D (Assignments)
- encoach.course.plan.assignment supports mode='batch' (op.batch) or
mode='students' (res.users), with due_date + message + state
- REST endpoints to list / create / delete assignments
Phase E (Student view)
- /api/student/course-plans + /api/student/course-plans/<id>
enforce visibility via assignment.expand_user_ids()
- New /student/course-plans list + read-only drilldown rendering
audio/image/video tiles from /web/content/<attachment_id>
Cross-cutting
- encoach.ai.tool.category: + media (so the new tools register)
- encoach.embedding gains a plan_id filter for plan-scoped RAG
- Wizard adds Sources + Multimedia steps; AdminCoursePlanDetail
rewritten with DeliverablesStrip + SourcesCard + AssignmentsCard +
per-material MediaDrawer
- ~280 new EN + AR i18n keys (full RTL coverage)
- smoke_course_plan.py exercises every phase via odoo-bin shell;
last run: PASS A/B/D/E + DALL-E 3 image (753 KB), Polly audio
fails cleanly when AWS creds aren't configured (expected)
Documentation: §24 added to docs/PROJECT_SUMMARY.md with phase-by-phase
artefact list, endpoints, smoke test, ops notes, and gotchas.
Made-with: Cursor
Based on UTAS GE1 Course Outline structure (Reading/Writing 10hrs + Listening/Speaking 8hrs)
New Models:
- encoach.course.plan.deliverable: Explicit learning outcome tracking by week/skill
- encoach.course.plan.resource.dep: Resource dependencies (textbooks, videos, etc.)
- encoach.course.plan.assignment: Assign plans to classes/students with progress tracking
- encoach.course.plan.assignment.deliverable: Per-student deliverable completion status
Extended Models:
- course.plan.material: Added media fields (media_type, media_asset_url, media_asset_id,
media_generation_prompt, media_metadata_json) for rich content
- New material types: video_lesson, audio_recording, image_visual, interactive, assessment
AI Agent Tools (agent_tools.py):
- deliverables.detect: Parse course outlines (like GE1 PDF) and extract structured outcomes
- deliverables.fetch: Get deliverables for AI to reference when generating
- resources.fetch: Check available resources before generating content
- resources.save: Persist resource dependencies
- media.suggest_visuals: AI suggests images/diagrams for materials
- media.generate_image: Generate educational images (DALL-E integration ready)
- media.generate_audio: Generate TTS audio (ElevenLabs/Polly integration ready)
- assignment.*: Create assignments and track progress
Pipeline Enhancements (course_plan_pipeline.py):
- generate_deliverables_from_outline(): Parse PDF/text outlines into structured deliverables
- generate_week_materials_with_resources(): Resource-aware content generation
- suggest_media_for_material(): AI visual aid suggestions
- generate_media_for_material(): Actual image/audio generation
New AI Agents (agents_defaults.xml):
- deliverable_detector: Parses GE1-style outlines, extracts deliverables week-by-week
- media_generator: Creates images/audio for teaching materials
- Updated course_planner & course_week_materials with resource tools
REST APIs (course_plan.py):
POST /api/ai/course-plan/<id>/deliverables/detect - Parse outline
GET /api/ai/course-plan/<id>/deliverables - List deliverables
PUT /api/ai/course-plan/deliverables/<id> - Update status
GET /api/ai/course-plan/<id>/resources - List resources
POST /api/ai/course-plan/<id>/resources - Add resource
POST /api/ai/course-plan/materials/<id>/media/suggest - Get visual suggestions
POST /api/ai/course-plan/materials/<id>/media/generate - Generate image/audio
POST /api/ai/course-plan/<id>/assignments - Assign to class/student
GET /api/ai/course-plan/<id>/assignments - List assignments
GET /api/ai/course-plan/assignments/<id> - Get with progress
PUT /api/ai/course-plan/assignments/<id>/deliverables/<del_id> - Update status
Security: Added ir.model.access.csv entries for all new models
Made-with: Cursor
Produces an Odoo-compatible <db>.zip restorable via /web/database/manager
on the live VPS (or curl -F backup_file=@... /web/database/restore).
Why this script instead of POST /web/database/backup:
the Odoo HTTP backup endpoint shells out to pg_dump from $PATH and on
this host that returns "Command pg_dump not found" because pg_dump
lives in the conda env that Odoo wasn't launched from. This script
calls pg_dump directly with PGBIN, copies the filestore, builds a
correct manifest.json (odoo_dump, db_name, version, pg_version,
modules), and zips the three artefacts in the exact layout Odoo's
restore code expects.
Layout matches Odoo's own backup output:
<db>.zip
|- dump.sql (pg_dump --no-owner --format=p)
|- manifest.json (odoo_dump=1, version, pg_version, installed modules)
|- filestore/ (data_dir/filestore/<db>/ contents)
Defaults are encoach_v2 / yamenahmad / /tmp/odoo-data, all overridable
via env vars (DB, DB_USER, DATA_DIR, PGBIN, OUT_DIR). Output filename
is encoach_v2_YYYYMMDD_HHMMSS.zip in ./backups/.
Verified: latest run produced encoach_v2_20260425_031949.zip — 30 MB
compressed, 815 entries, 633 tables x 633 COPY data blocks, 575
filestore files, 94 modules in manifest.
Made-with: Cursor
Explains why /web/database/manager restores appear to succeed but the
live site keeps serving the old data (target DB name mismatch / Odoo
not restarted / dbfilter pinned), and ships a 6-step SSH-based
recovery procedure with safety-net backup, filestore handling, and
verification checklist tied to the known QA fixtures.
Made-with: Cursor
Production QA reported "Submit failed 413" on /generation and plain
"Upload failed" (no detail) on resource upload. Root cause is the
outer reverse-proxy nginx on the VPS — still on default
`client_max_body_size 1m` — rejecting requests before they reach
Odoo. The inner docker-nginx and Odoo limits were already raised in
earlier commits; only the VPS proxy was left unpatched.
UI side
- Add `describeApiError(err, fallback, context)` helper in
lib/api-client.ts. Maps common HTTP codes to human-readable,
actionable toast descriptions (413 → "ask ops to raise nginx
client_max_body_size", 504 → "server took too long, retry", 502
→ "Odoo is restarting", 401 → "sign in again", 4xx/5xx → server
error body + status code).
- Use it in GenerationPage submit, ResourceManager upload,
TeacherLibrary upload, CustomExamCreate save/publish. Replaces
four slightly-different ad-hoc mappings with one source of truth.
Deploy side
- Add deploy/nginx-vps.conf.example: full TLS reverse-proxy template
with the body/timeout knobs that must match Odoo's limit_request
(128 MB) and limit_time_real (900 s).
- Add docs/DEPLOY_413_504_FIX.md: step-by-step remediation guide for
the team lead covering the 3 layers that can block large bodies
(outer nginx, Cloudflare, Odoo worker) and how to verify each.
Made-with: Cursor
E2E QA surfaced a silent-drop bug in the exam-schedule endpoint: the
frontend's student picker sends `op.student.id` values (that's what
/api/students returns), but `encoach.exam.schedule.student_ids` is a
Many2many → res.users. The controller was writing the op.student id
straight through, so schedules linked to whichever res.users row
happened to share that integer id (OdooBot, other staff, etc.) — the
target student got 0 assignments and never saw the exam.
- exam_schedules.py: add `_resolve_student_user_ids` and use it in the
create and update endpoints. Falls back to treating unmatched ids as
direct res.users ids for back-office callers.
- AssignmentsPage.tsx: add a search input + larger list to the
Individual Students picker; long lists were leaving Sarah unreachable
inside the inner scroll container during QA.
Made-with: Cursor
First-visit users now always land on the English UI regardless of
navigator.language. Arabic remains one click away via the toggle, and
the user's explicit pick is persisted to localStorage (encoach-lang)
and honoured on every subsequent load.
- src/i18n/index.ts: set lng: "en", drop navigator/htmlTag from the
detector order so an Arabic-locale browser no longer silently boots
the UI in Arabic before the user has chosen.
- src/lib/api-client.ts: mirror the same policy for the Accept-Language
header sent to the backend, so AI-generated content stays English on
first visit instead of echoing the browser locale.
Made-with: Cursor
Addresses the QA notes on the end-to-end approval flow. Highlights:
Backend
- encoach_ai/generation_submit: create encoach.approval.request on submit so
submitted exams actually reach the approver queue (previously only the
workflow id was stored on the exam, leaving approvers with an empty inbox).
Initial exam status flips to pending_approval instead of draft.
- encoach_exam_template/approval_workflows:
* /api/approval-users filters out students (user_type='student',
op_student_id set, share=True) so the approver dropdown only lists staff.
* _ser_request enriches requests with target_name / target_status and the
current stage approver for UI badges.
* list_requests supports mine=1 / requester=1 so approvers only see queue
items awaiting their action.
* approve_request / reject_request now transition the underlying
encoach.exam.custom to published / rejected on final approval.
- encoach.exam.custom.status: add pending_approval and rejected states to
match the approval workflow transitions.
Frontend UX
- GenerationPage: rubric field shows "Auto-graded — no rubric" for Listening
and Reading (rubrics only apply to Writing / Speaking). Divider dropdowns
now carry explicit help text explaining None / Line / Space / Page Break
and where to write prompts. Selecting an official exam structure
auto-populates the required tasks/sections/parts, the delete button is
hidden for essential tasks, and submit is blocked if the user dropped
below the structure's required count.
- RubricsPage: "Add Criterion" is now a dropdown with IELTS-specific
presets (Task Achievement, Coherence & Cohesion, Fluency & Coherence, …)
keyed off the selected skill, plus a "Custom (blank row)" fallback.
- AdminCourses: added tooltips and helper copy clarifying Difficulty vs
CEFR Level in the Create Course dialog.
- CustomExamCreate: validate the whole form before Save/Publish, surface
backend error messages (413/504/401) instead of a generic toast, read the
exam id from the correct response field, and avoid marking Publish as
failed when only the optional Save-as-Template step errors.
- ResourceManager / TeacherLibrary: upload toast now shows a concrete
reason (HTTP 413 / 504 / 401) instead of a generic "Upload failed".
Config (504 / 413 / resource upload fixes)
- odoo.conf + odoo-docker.conf: raise limit_time_cpu to 600s,
limit_time_real to 900s, limit_request to 128 MB, and memory caps so
/api/exam/generation/submit and multipart resource uploads stop hitting
504 / 413 during QA.
- frontend/Dockerfile (nginx): add client_max_body_size 128m, bump
proxy_read_timeout / proxy_send_timeout to 900s, and disable request
buffering so large AI/resource payloads stream through the proxy.
Made-with: Cursor
Database dumps list 12 openeducat_* modules as installed (openeducat_core,
openeducat_admission, openeducat_assignment, openeducat_attendance,
openeducat_activity, openeducat_classroom, openeducat_exam, openeducat_facility,
openeducat_fees, openeducat_library, openeducat_parent, openeducat_timetable),
but the `backend/openeducat_erp-19.0/` folder was previously in .gitignore.
When a teammate restored the dump on the VPS Odoo failed with "module not
found" on every openeducat_* row in ir_module_module.
- Remove `backend/openeducat_erp-19.0/` from .gitignore and vendor the full
LGPL-3 OpenEduCat Community 19.0 tree (14 modules: 12 core + theme + erp
meta-module). `odoo.conf` and `odoo-docker.conf` already reference this
path in `addons_path`, so restores now succeed on a fresh VPS with no
extra bootstrap step.
- Strip the nested .git metadata that came with the upstream download so
the tree is vendored flat (not as a submodule pointer).
Made-with: Cursor
Frontend
- i18n: install tailwindcss-rtl, Cairo font, RTL-aware direction in index.css.
- Language toggle: localize aria-label / menu label, persist choice, update
document dir synchronously.
- Sidebar: add `side` prop so the drawer pins to the right in RTL; wire up
AdminLmsLayout, RoleLayout (student/teacher) and AppSidebar to pass
side = i18n.dir() === 'rtl' ? 'right' : 'left'.
- AdminLmsLayout: convert every nav item from hard-coded title to titleKey,
translate group labels (incl. the collapsible Training), breadcrumbs,
user menu (Profile / Settings / Logout), help button and toggle aria
labels; replace physical mr-/right- utilities with logical me-/end-.
- AI components (AiTipBanner, AiInsightsPanel, AiAlertBanner, AiSearchBar,
AiAssistantDrawer): apply dir="auto" at the container level, localize
titles, loading / error / empty states.
- Dashboards (admin / student / teacher): wrap numeric values in <bdi>,
localize dates via ar-EG, fix flex direction for KPI and assignment cards.
- UI primitives (breadcrumb, calendar, carousel, dropdown-menu, menubar,
context-menu, pagination, sidebar): flip chevrons in RTL via a scoped
CSS rule, swap pl-/pr-/ml-/mr- for ps-/pe-/ms-/me-.
- Add logical-direction helpers and bidirectional isolation classes.
Locales
- Expand en.ts and ar.ts with full `nav`, `sidebarGroup`, `breadcrumb`,
`userMenu`, `chrome`, `ai`, and dashboard key sets; keep key parity.
API client
- `api-client.ts` reads the active language from localStorage/i18n and sends
`Accept-Language` on every request so the backend can localize AI output.
Backend (encoach_ai)
- openai_service: add _LANGUAGE_NAMES, normalize_language, language-aware
system prompt injection for every OpenAI call.
- coach_service + controllers (coach_controller, ai_controller): thread
the requested language from headers / user locale down to OpenAIService.
- ai_feedback: fix latent registry error by pointing course_id at op.course
instead of the non-existent encoach.course.
Other
- .gitignore: ignore runtime odoo logs and local caches.
Made-with: Cursor