Update PROJECT_SUMMARY with the Professional Interactive Course Plans delivery, scanned-PDF OCR indexing remediation, regression/browser verification results, and the current OpenAI insufficient_quota fallback behavior. Made-with: Cursor
133 KiB
EnCoach Platform — Project Summary
Last updated: 2026-04-27 | Canonical repos:
encoach_backend_v4(backend) +encoach_frontend_v4(frontend), branchmain.This workspace (
odoo19/) is a developer monorepo / working tree only — it conveniently contains both halves side-by-side for local development and testing. The two split repos above are the authoritative origins for each half: every change must be published to them (viagit subtree split + push) before the team lead can deploy. See §6 Git Remotes & Repositories for the exact workflow.
Latest events:
- 2026-04-27 (professional interactive course plans + scanned-PDF OCR indexing): Completed the end-to-end "Professional Interactive Course Plans" rollout and validated it in API smoke + browser runs. Backend: added
RAGContextBuilder, richer v2 week-material schema (/api/ai/course-plan/<id>/weeks/<n>/materials/v2),interactive_workbookmaterial type, provenance fields (grounded_on_json,extracted_from_json), workbook extraction service, and attempt persistence/scoring model (encoach.course.plan.workbook.attempt) with new endpoints for extraction, grounding, and attempts. Frontend: addedInteractiveWorkbook, sharedPlanReader,GroundingBadge, and true admin "View as Student" preview mode rendering the same student reader without requiring a student login. Operational fix: scanned exercise books (e.g. Headway Intermediate workbook) were failing withExtracted no text from source; implemented OCR fallback insource_indexer.py(tesseract + poppler +pdf2image/pytesseract) with per-page streaming to keep memory bounded, then re-indexed failed sources successfully (indexed, 109 chunks, ~207k chars each). Regression checks still pass (smoke_assignment_workflow.py,smoke_entity_isolation.py,smoke_course_plan_rag.py) and browser verification confirms sources now show greenIndexedbadges. Current environment note: OpenAI course-generation calls are returning429 insufficient_quota, so "Regenerate week materials" currently writes the intended skeleton fallback content with an explicit in-body note until API billing/quota is restored.- 2026-04-26 (dynamic course-section UX completion): Wired the dynamic
Course → Sections → Classroom → Batchstructure all the way through the admin UX. New idempotent backend endpointPOST /api/courses/<id>/sections/generate-defaultscreates the canonical A/B/C templates in one call (custom codes also supported via{ "codes": [...] }).POST /api/batchesnow auto-generates a uniquecode(built from course + section + term) when the client omits one — fixing the previous NOT-NULL/uniqueness failure onop.batch.codeand unblocking the AdminBatches form flow. New frontend service helpersgenerateDefaultCourseSections,updateCourseSection,deleteCourseSection.AdminCoursesnow shows a Sections column with the live count + section codes + a “Manage Sections” dialog (list / add / inline edit / delete + one-clickGenerate A · B · C).AdminBatchesexposesSection+Term Keycolumns plus matching selectors in both Create and Edit dialogs (the section list is fetched per-course and disabled until a course is picked). Smoke-tested live: generate-defaults created A/B/C (idempotent re-run added only the newD), section PATCH/DELETE worked, batch was created/updated/validated against course mismatch (400 course_section_id does not belong to course_id), and the Edit dialog hydratedcourse_section_id+term_keycleanly. Combined with the previous classroom-side cascade, the platform now fully implements the diagram: any classroom can host any sections from any courses, each producing exactly one canonical batch per(classroom × course × section × term)with roster + teachers auto-propagated.- 2026-04-26 (dynamic course-section classroom structure): Implemented a dynamic Course -> Sections -> Classroom hosting model aligned to the LMS diagrams. Added new backend model
encoach.course.section(per-course section templates, unique code per course, optional branch) with API CRUD routes inencoach_lms_api/controllers/lms_core.py:GET/POST /api/courses/<course_id>/sections,PATCH/DELETE /api/courses/<course_id>/sections/<section_id>. Extended batches withcourse_section_id+term_key, and classroom cascade assignment now supportssection_id/term_keyto create or reuse canonical batches per(classroom × course × section × term). Added explicit alias routePOST /api/classrooms/<id>/assign-sectionand newGET /api/classrooms/<id>/sections. Frontend types/services/pages updated accordingly: course section types, section-aware classroom assignment flow, and batches UI now shows section metadata. Smoke-tested successfully: same classroom received Section A and Section B of the same course, producing two distinct batches with roster auto-propagation.- 2026-04-26 (admin-only entity membership management UI): Added end-to-end UI flow for linking users to entities directly from
Admin -> Entities(admin only). New backend routes inencoach_exam_template/controllers/entities.py:GET /api/entities/<entity_id>/usersandPATCH /api/entities/<entity_id>/users(payload{ user_ids: [...] }) with strict admin guard (base.group_system/base.group_erp_manager/user_type=admin).EntitiesPagenow includes a Manage Users action (UserCog) opening a searchable multi-select dialog, andentities.service.tsnow exposeslistEntityUsers,updateEntityUsers, andlistPlatformUsershelpers. Verified on live:8069: admin gets200, non-admin gets403, and/api/userreflects newly linked entities after re-login so the header switcher shows them.- 2026-04-26 (frontend entity switcher + automatic scope propagation): Added active-entity selection to the authenticated frontend session.
AuthContextnow tracksselectedEntityId/selectedEntity, persists it in local storage (encoach_entity_id), auto-validates it against the logged-in user’s entity memberships, and resets it on logout.AdminLmsLayoutnow exposes an entity switcher in the header (for users with entities) and refreshes route data after switching.api-clientnow auto-injectsentity_idfor entity-scoped LMS endpoints (/courses,/students,/teachers,/batches,/student/my-courses) in query params and request bodies when the caller didn’t pass one explicitly. This complements the backend guardrails so managers can intentionally switch between isolated entity LMS contexts from UI while backend remains the source of truth.- 2026-04-26 (entity-isolated LMS backend guardrails): Implemented server-side entity isolation for core LMS APIs so users only see/manage records within their linked entities. Added
entity_idownership fields toop.batch,op.student,op.faculty(and keptop.course.entity_idin this module extension), then enforced scoped domains + access checks inencoach_lms_api/controllers/lms_core.pyacross courses/students/teachers/batches and enrollment flows. Creation now auto-defaults to the caller’s first entity whenentity_idis omitted (non-super users), supports explicitentity_idwith access validation, and returns 403 on cross-entity access attempts. Serializers now exposeentity_id/entity_namefor these LMS records. Verified locally after module upgrade (-u encoach_lms_api): non-super user with entity{1}sees only entity1courses and gets403for?entity_id=3.- 2026-04-26 (course-plan material authoring + entity assignment): Upgraded AI course-plan delivery UX from raw JSON to an editable, book-style material experience. Added
PATCH /api/ai/course-plan/material/<id>to edit generated material title/summary/body text, plus new metadata fields onencoach.course.plan.material:share_date(editable) andis_static(preserved on regenerate). Week regeneration now keeps static rows (generate_week_materialsonly replaces non-static materials). Added multi-entity assignment support with new assignment modeentities(entity_ids), including API create/list payloads and student visibility expansion via entity membership. Admin detail now supports student-preview mode, per-skill color badges + filtering, simple content editing (not code view), share-date/static controls, and entity picker in assignment dialog. Student detail now renders the same book-style material view with per-skill filters and authenticated media preview URLs. Module upgraded locally with-u encoach_ai_course; API smoke verified: material patch + entity assignment both pass.- 2026-04-25 (full demo seed + 8-role E2E): Filled every product
user_typewith believable demo data and ran end-to-end smoke + mutation tests across all eight roles. New idempotent seeders (seed_full_demo.py,reset_demo_passwords.py) add the 5 missing user types (approver,corporate,mastercorporate,agent,developer), an active 2-stage exam-approval workflow with one pending request, and a full GE1-aligned B1 course plan modelled on the UTAS General English 1 Fall AY25-26 outline (12 weeks, 6 detailed week-1 materials covering reading / writing / listening / speaking / grammar / vocabulary). Newe2e_full_scenario.pyexercises the API surface for each role (46/46 PASS, 0 fail across admin/teacher/approver/student/corporate/mastercorporate/agent/developer) ande2e_approval_chain.pywalks the full mutation path: approver approves stage 1 → admin approves stage 2 → linked exam auto-published. Live LangGraph round-trips verified during the run (writing_grader 3.3 s, lms_tutor ReAct with 2 real tool calls 13 s). Full QA write-up indocs/ENCOACH_FULL_DEMO_QA_REPORT.md. See §23.- 2026-04-25 (LangGraph as core AI runtime): Made LangGraph the backbone for every AI feature on the platform — course planning, exam/exercise generation, LMS tutor, writing/speaking grading. New
encoach.ai.agent+encoach.ai.toolOdoo models (M2M tool binding, graph type, model, temperature, fallback model, max revisions, quality checks, system prompt, prompt key, response format). Newservices/agent_runtime.pycompiles each agent into aStateGraphwith four topologies (simple,plan_review_revise,rag,react) andservices/agent_tools.pyships an 11-tool registry wrapping existing services (vector search, rubric/outcomes/student fetch, CEFR/AI-detect/content-gate, course-plan persistence, writing/speaking grading). 7 default agents seeded viadata/agents_defaults.xml. New/api/ai/agents*controller (list/get/update/test, list-tools, toggle-tool). The page at/admin/ai/promptsis now a tabbed Agents | Tools | Prompts console with a config dialog (graph type, model, temperature, fallback, max revisions, quality checks, tool toggles) and a built-in Test Runner that shows output + tool trace + retrieval hits + revisions + quality issues. EN + AR (RTL) translations for every new string. TheCoursePlanPipelinenow routes throughAgentRuntimewhenencoach_ai.use_langgraph_runtimeis on. See §22.- 2026-04-19 (reports section): Built the Reports section end-to-end — the three pages
/admin/student-performance,/admin/stats-corporate,/admin/record(previously pure hardcoded-array mocks) are now wired to real aggregated data fromencoach.student.attempt. New/api/reports/{student-performance,stats-corporate,record,filters}controller (encoach_lms_api/controllers/reports.py) does the rollups: per-student band averages + CEFR, per-module corporate charts, trend / distribution / entity comparison, and per-user attempt history with search / level / entity / period filters and CSV export. Newseed_reports.pycompletes in-progress attempts and backfills six months of historical attempts so the trend chart and KPI cards are meaningful. 25/25 API smoke passing (test_reports_flows.py), 24/24 Configuration + 29/29 Support + 26/26 Training regressions still green, all three pages verified live in-browser with 28 real attempts showing across 4 tabs. See §20.- 2026-04-19 (remote rename): Aligned local remote names with the new doctrine —
backend-v4 → origin-backend,frontend-v4 → origin-frontend,origin → mirror-monorepo. Thev4branch now tracksmirror-monorepo/v4. All publishing commands in §6 updated. No remote URLs changed.- 2026-04-19 (repos-of-record reorganization): Declared
encoach_backend_v4andencoach_frontend_v4as the canonical origins for their respective halves; the monorepoencoach_backend_new_v2/v4is now a secondary working-tree mirror kept only for history/convenience. §6 rewritten around this model.- 2026-04-19 (release to VPS repos): Pushed the accumulated institutional + support + training work. Frontend canonical →
encoach_frontend_v4/main(b78124bb..435930a8, 403 files, clean fast-forward). Backend canonical →encoach_backend_v4/main(74d83af5..6ec68160, 420 files, clean fast-forward). Monorepo mirror →mirror-monorepo/v4(98b9837a)..gitignorenow excludespgdata_bak_*/,frontend/.vite/,frontend/dist/,frontend/node_modules/so the 674 MB of local pgdata backups never leak into a push. See §6.- 2026-04-18 (training section): Built the Training section end-to-end — the
VocabularyandGrammarpages (previously pure hard-coded mocks) are now wired to real Odoo data with CRUD, search, level filtering, and per-user progress tracking. Newencoach.vocab.item/encoach.vocab.progress/encoach.grammar.rule/encoach.grammar.progressmodels +/api/training/vocabularyand/api/training/grammarcontrollers. 26/26 API write-flow actions passing (test_training_flows.py); both pages verified live in-browser. See §19.- 2026-04-18 (support section): Built the Support section end-to-end — the
Tickets,Payment Record, andSettingspages are now wired to real Odoo data (they were previously mocks/404s). Newencoach.ticketmodel + full CRUD controller,/api/payment-recordsderiving fromop.student.fees.details+account.move,/api/codes+/api/packages+/api/grading-configpowering the Settings tabs. 29/29 API write-flow actions passing (test_support_flows.py); all 3 pages verified live in-browser. See §18.- 2026-04-18 (institutional pages write-flow): API-level write-flow sweep across all 14 institutional admin pages — 44/44 actions passing (CREATE / EDIT / DELETE / WORKFLOW). Numerous backend gaps fixed: missing DELETE routes added (admission-registers, admissions, result-templates, student-leaves), lesson/library detail endpoints added, custom
encoach.assetmodel introduced to replace the missingop.asset, marksheet generate endpoint aliased for Odoo 19'sgenerate_result, feescreate-invoiceauto-wires the product income account via Odoo 19's JSONB property storage, academic-year DELETE now cascades dependent terms. See §17.- 2026-04-18 (browser E2E): Full end-to-end test across admin, teacher, student user types. All critical pages verified working in browser. Two backend bugs fixed: (1)
/api/approval-workflowsreturned 500 because raw SQL referenced a table that doesn't exist — now gracefully returns empty list; (2)DELETE /api/courses/<id>returned opaque 500 when course had enrollments — now returns 409 with a clear message and supports?force=truefor cascade-delete. See §16.
1. Project Overview
EnCoach is an AI-powered online learning and examination platform built on Odoo 19 (backend) and React + TypeScript + Vite (frontend). It supports IELTS and general English exam generation, student exam sessions, auto-scoring, adaptive learning, and course creation.
2. Tech Stack
| Layer | Technology |
|---|---|
| Backend | Odoo 19, Python 3.12, PostgreSQL 18 |
| Frontend | React 18, TypeScript, Vite 5, Tailwind CSS, Shadcn UI |
| State Management | @tanstack/react-query (mutations + queries) |
| Auth | JWT (issued by Odoo, validated via validate_token helper) |
| AI | OpenAI API (with fallback templates when API key not configured) |
| Database | encoach_v2 (PostgreSQL 18.3) |
3. User Credentials
Every product user_type is now represented in the demo data. After running seed_full_demo.py + reset_demo_passwords.py, the canonical credentials are:
| ID | Login | Name | user_type | Password |
|---|---|---|---|---|
| 2 | admin |
Administrator | superadmin | admin |
| 5 | admin@encoach.test |
Admin User | admin | admin123 |
| 6 | sarah@encoach.test |
Sarah Ahmed | student | student123 |
| 7 | omar@encoach.test |
Omar Khan | student | student123 |
| 8 | layla@encoach.test |
Layla Nasser | student | student123 |
| 9 | khalid@encoach.test |
Dr. Khalid | teacher | teacher123 |
| 10 | fatima@encoach.test |
Ms. Fatima | teacher | teacher123 |
| 13 | approver@encoach.test |
Approver Coach | teacher (approver) | approver123 |
| 14 | corporate@encoach.test |
Acme Corporate | corporate | corporate123 |
| 15 | master@encoach.test |
Master Group HQ | mastercorporate | master123 |
| 16 | agent@encoach.test |
Sales Agent | agent | agent123 |
| 17 | dev@encoach.test |
Platform Dev | developer | dev123 |
Reset flow: Re-run
reset_demo_passwords.pyagainst any database to re-apply these passwords (idempotent, uses Odoo ORM viaodoo-bin shell). The product supports 7user_typevalues:student,teacher,admin,corporate,mastercorporate,agent,developer. Approver is not a separateuser_type— it's ateacherlinked to a stage inencoach.approval.stage. See §23 for the full demo dataset and E2E run.
Login API
# Admin login (note: API field is `login` for the demo accounts)
curl -X POST http://localhost:8069/api/login \
-H "Content-Type: application/json" \
-d '{"login":"admin@encoach.test","password":"admin123"}'
# Student login
curl -X POST http://localhost:8069/api/login \
-H "Content-Type: application/json" \
-d '{"login":"sarah@encoach.test","password":"student123"}'
# Approver login (drives the §23 approval-chain E2E)
curl -X POST http://localhost:8069/api/login \
-H "Content-Type: application/json" \
-d '{"login":"approver@encoach.test","password":"approver123"}'
Returns { token, user, permissions }. The token (JWT) is used as Authorization: Bearer <token> for all subsequent API calls.
4. How to Start the Project
Prerequisites
- PostgreSQL 18 binary at
/Users/yamenahmad/micromamba/envs/odoo19/bin/ - Python 3.12 (conda env at
.conda-envs/odoo19/or micromamba) - Node.js 20 at
/Users/yamenahmad/.local/node/bin/
Start PostgreSQL
PGBIN="/Users/yamenahmad/micromamba/envs/odoo19/bin"
PGDATA="/Users/yamenahmad/projects2026/odoo/odoo19/pgdata"
"$PGBIN/pg_ctl" -D "$PGDATA" -l "$PGDATA/logfile" start
Important: The pgdata was initialized with PG 18. The conda env has PG 17.6, which is incompatible. Always use the micromamba PG 18 binary above.
If PG fails with config errors, check
pgdata/logfile. The parameterautovacuum_worker_slots(PG 18-only) was commented out inpgdata/postgresql.confline 687 for compatibility.
Start Odoo Backend
cd /Users/yamenahmad/projects2026/odoo/odoo19
./run.sh
Runs at http://127.0.0.1:8069
Odoo config (
odoo.conf) must havedb_name = encoach_v2anddbfilter = ^encoach_v2$, otherwise API routes return 404 ("No database selected").
Start Frontend
export PATH="/Users/yamenahmad/.local/node/bin:$PATH"
cd /Users/yamenahmad/projects2026/odoo/odoo19/frontend
npm run dev
Runs at http://localhost:8080 — Vite proxies /api requests to Odoo :8069.
Stop PostgreSQL
"$PGBIN/pg_ctl" -D "$PGDATA" stop
5. Project Structure
odoo19/
├── backend/
│ ├── custom_addons/ # All EnCoach Odoo modules
│ │ ├── encoach_api/ # Auth (login/logout/user) + base helpers
│ │ ├── encoach_ai/ # AI generation, exam generation/submit
│ │ ├── encoach_ai_course/ # AI course creation pipelines
│ │ ├── encoach_adaptive/ # Adaptive learning engine
│ │ ├── encoach_core/ # Core models (user, entity, role, permission)
│ │ ├── encoach_exam_template/ # Exam CRUD, sessions, rubrics, assignments, schedules
│ │ ├── encoach_scoring/ # Grading queue, score release
│ │ ├── encoach_taxonomy/ # Subject/topic tree
│ │ ├── encoach_lms_api/ # LMS: courses, chapters, materials, progress, enrollment
│ │ ├── encoach_course_gen/ # Course generation
│ │ ├── encoach_branding/ # Entity branding
│ │ ├── encoach_signup/ # Registration, email verification, onboarding
│ │ ├── encoach_placement/ # Placement test
│ │ └── ...
│ └── openeducat_erp-19.0/ # OpenEduCat base modules
├── frontend/
│ ├── src/
│ │ ├── pages/ # Route pages (admin/, student/, etc.)
│ │ ├── components/ # Reusable UI components
│ │ ├── services/ # API service classes
│ │ ├── hooks/ # React Query hooks
│ │ ├── lib/ # api-client, utils
│ │ └── types/ # TypeScript type definitions
│ ├── .env.development # VITE_API_BASE_URL=/api
│ └── .env.production # VITE_API_BASE_URL=https://api.encoach.com/api
├── docs/ # Documentation
├── odoo/ # Odoo 19 source code
├── odoo.conf # Odoo configuration
├── pgdata/ # PostgreSQL data directory (PG 18)
├── run.sh # Start Odoo
├── start.sh # Start PG + Odoo
└── stop.sh # Stop services
6. Git Remotes & Repositories
Canonical (repos-of-record):
- Backend →
encoach_backend_v4— branchmain- Frontend →
encoach_frontend_v4— branchmainThese are the repositories the team lead (Talal / devops) clones on the VPS and builds with
docker compose up -d --build. All production / staging code lives here.This workspace (
odoo19/) is a developer monorepo / working tree — a convenience folder that holds both halves side-by-side so a single developer can run the full stack locally. It is not the source of truth: every change must be published to the two canonical repos above viagit subtree split + pushbefore it counts as merged.
6.1 Repository Layout
odoo19/ ← developer working tree (NOT source of truth)
├── backend/ → published to encoach_backend_v4/main (authoritative)
│ ├── custom_addons/
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── requirements.txt
├── frontend/ → published to encoach_frontend_v4/main (authoritative)
│ ├── src/
│ ├── public/
│ ├── Dockerfile, vite.config.ts, package.json, …
└── docs/ → lives only in this workspace
6.2 Remotes Configured on This Clone
Listed in order of importance. The two bold rows are the repos of record; everything else is a secondary mirror or legacy.
| Remote | URL | Role |
|---|---|---|
origin-backend |
https://git.albousalh.com/devops/encoach_backend_v4.git |
Canonical backend — branch main. VPS deploy target. |
origin-frontend |
https://git.albousalh.com/devops/encoach_frontend_v4.git |
Canonical frontend — branch main. VPS deploy target. |
mirror-monorepo |
https://git.albousalh.com/devops/encoach_backend_new_v2.git |
Secondary full-tree mirror (branch v4 tracked here). Useful for history/backup and for docs that live only in the workspace root (e.g. this file). Not the source of truth. |
ip-origin |
https://5.189.151.117/devops/encoach_backend_new_v2.git |
IP fallback for mirror-monorepo when DNS is flaky. |
backend-v3 |
https://git.albousalh.com/devops/encoach_backend_new_v3.git |
Legacy v3 monorepo mirror — rarely used. |
frontend-v3 |
https://git.albousalh.com/devops/encoach_frontend_new_v3.git |
Legacy v3 frontend mirror — rarely used. |
frontend |
https://git.albousalh.com/devops/encoach_frontend_new_v2.git |
Legacy v2 frontend monorepo — rarely used. |
Rename history:
backend-v4 → origin-backend,frontend-v4 → origin-frontend,origin → mirror-monorepo(applied 2026-04-19). If you have old scripts or notes referencing the old names, update them or add aliases viagit remote rename.
6.3 Feature Development Workflow
Everything is edited inside this workspace, but a change is only merged
once it lands on main of the two canonical repos.
# 1. Start from a clean workspace synced with the monorepo mirror
git checkout v4
git pull mirror-monorepo v4 # optional: refresh full-tree mirror
# 2. Create a feature branch
git checkout -b feature/<short-name>
# 3. Make changes, commit small and often
git add <files>
git commit -m "feat(<area>): <short description>"
# 4. Merge back onto v4 (or open a PR if branch protection is enabled)
git checkout v4
git merge --no-ff feature/<short-name>
# 5. Publish to the CANONICAL repos (mandatory — see §6.4)
# This is what the team lead / CI will see on the VPS.
# 6. (Optional) push the monorepo mirror for a full-tree backup / docs
git push mirror-monorepo v4
6.4 Publishing to the Canonical Repos (MANDATORY)
The canonical repos expect a flat tree (no backend/ or frontend/
prefix). Use git subtree split to carve each subdirectory into its own
parallel history, then push to main. These commands produce clean
fast-forwards when nothing has been pushed directly to the canonical repos
from elsewhere — which is the invariant we enforce.
# ── Publish BACKEND → encoach_backend_v4 ─────────────────────────────────
git subtree split --prefix=backend HEAD -b split-backend-v4
git push origin-backend split-backend-v4:main
git branch -D split-backend-v4 # cleanup the temp branch
# ── Publish FRONTEND → encoach_frontend_v4 ───────────────────────────────
git subtree split --prefix=frontend HEAD -b split-frontend-v4
git push origin-frontend split-frontend-v4:main
git branch -D split-frontend-v4
If a push is rejected as non-fast-forward, fetch and inspect before forcing:
git fetch origin-backend origin-frontend
git log --oneline origin-backend/main..split-backend-v4 # what we're adding
git log --oneline split-backend-v4..origin-backend/main # divergence, if any
Only use --force after confirming the remote main has no commits worth
keeping (e.g. someone pushed directly and it must be overwritten). Since the
canonical repos are the source of truth, a force-push there is a destructive
production operation — pair it with a Slack heads-up to the team lead.
6.5 What Each Canonical Repo Contains
encoach_backend_v4 (branch main) — everything under backend/ hoisted
to repo root:
custom_addons/(all EnCoach Odoo addons +encoach_lms_api)Dockerfile,docker-compose.yml,requirements.txt
Not shipped: openeducat_erp-19.0/ is vendored locally but not tracked in
git. The VPS needs OpenEduCat installed separately — either baked into the
container image, mounted at /opt/odoo/extra_addons, or installed via pip if a
package release is used. backend/odoo.conf is also not currently tracked; add
it in a follow-up commit if the VPS container needs a baked-in config.
encoach_frontend_v4 (branch main) — everything under frontend/ hoisted
to repo root:
src/,public/,docs/(frontend docs only)package.json,package-lock.json,bun.lock,bun.lockbDockerfile,docker-compose.ymlvite.config.ts,tsconfig.*.json,tailwind.config.ts,eslint.config.jsindex.html,components.json,.env.example
Not shipped (ignored by frontend/.gitignore): node_modules/, dist/,
.vite/, .env, .env.development, .env.production.
6.6 VPS Deploy — Team-Lead Quickstart
# Backend
git clone https://git.albousalh.com/devops/encoach_backend_v4.git
cd encoach_backend_v4 && docker compose up -d --build
# Frontend
git clone https://git.albousalh.com/devops/encoach_frontend_v4.git
cd encoach_frontend_v4 && cp .env.example .env # set VITE_API_BASE_URL
docker compose up -d --build # or: npm ci && npm run build
6.7 Safety Rules (DO NOT COMMIT)
Already excluded via .gitignore — do not force them in:
pgdata/,pgdata_bak_*/— PostgreSQL data directories (up to ~700 MB)frontend/node_modules/,frontend/dist/,frontend/.vite/— build artifacts.env,.env.development,.env.production— local secrets.venv/,venv/,.conda-envs/— Python virtualenvs*.zip,*.tar.gz— archives
odoo.conf at the workspace root IS tracked (local Mac dev config, harmless).
backend/odoo.conf (container-friendly) is not tracked; add it in a
follow-up if the VPS image needs it baked in.
6.8 Git Credentials
Stored in macOS Keychain for git.albousalh.com:
- Username:
yamen - Auth: token-based (stored in keychain, not in this file)
7. API Routes Reference
Authentication (encoach_api)
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/login |
public | Login, returns JWT |
| GET | /api/user |
JWT | Get current user |
| POST | /api/logout |
public | Logout (client clears token) |
Exam Generation & AI (encoach_ai)
| Method | Route | Description |
|---|---|---|
| POST | /api/exam/<module>/generate |
AI-generate content for a module (reading/listening/writing/speaking) |
| POST | /api/exam/generation/submit |
Submit generated exam, create questions |
| POST | /api/exam/<module>/generate/save |
Save generated content |
Exam Session (encoach_exam_template)
| Method | Route | Description |
|---|---|---|
| GET | /api/exam/<exam_id>/session |
Get exam session (creates attempt if needed) |
| POST | /api/exam/<exam_id>/autosave |
Autosave student answers |
| POST | /api/exam/<exam_id>/submit |
Submit exam for scoring |
| GET | /api/exam/<exam_id>/status |
Get attempt status |
| GET | /api/exam/<exam_id>/results |
Get exam results |
Custom Exams
| Method | Route | Description |
|---|---|---|
| GET | /api/exam/custom/list |
List custom exams |
| POST | /api/exam/custom/create |
Create custom exam |
| GET/PUT/DELETE | /api/exam/custom/<id> |
CRUD on custom exam |
Exam Structures, Templates, Schedules, Assignments
| Method | Route | Description |
|---|---|---|
| GET/POST | /api/exam-structures |
List/create exam structures |
| GET/POST | /api/exam/templates |
List/create exam templates |
| GET/POST | /api/exam-schedules |
List/create schedules |
| GET | /api/student/my-exams |
Student's assigned exams |
| GET/POST | /api/assignments |
List/create assignments |
Rubrics & Approval Workflows
| Method | Route | Description |
|---|---|---|
| GET/POST | /api/rubrics |
List/create rubrics |
| GET/POST | /api/rubric-groups |
List/create rubric groups |
| GET/POST | /api/approval-workflows |
List/create approval workflows |
| POST | /api/approval-requests/<id>/approve |
Approve request |
| POST | /api/approval-requests/<id>/reject |
Reject request |
Entities
| Method | Route | Description |
|---|---|---|
| GET | /api/entities |
List entities |
| GET | /api/entities/<entity_id>/users |
List users assigned to the entity (admin only) |
| PATCH/PUT | /api/entities/<entity_id>/users |
Replace assigned entity users with user_ids (admin only) |
LMS Core (encoach_lms_api — lms_core.py)
| Method | Route | Description |
|---|---|---|
| GET/POST | /api/courses |
List all courses (paginated) / create course |
| GET/PUT/DELETE | /api/courses/<id> |
Get / update / delete a single course |
| GET/POST | /api/students |
List students / create student |
| GET/PUT/DELETE | /api/students/<id> |
Get / update / delete a student |
| GET | /api/student/my-courses |
Enrolled courses for logged-in student (with progress) |
| POST | /api/students/<id>/enroll |
Enroll a student in one or more courses |
| POST | /api/courses/<id>/enroll |
Bulk-enroll students in a course |
| GET/POST | /api/teachers |
List / create teachers |
| GET/PUT/DELETE | /api/teachers/<id> |
Get / update / delete a teacher |
| GET/POST | /api/batches |
List / create batches |
| GET/PUT/DELETE | /api/batches/<id> |
Get / update / delete a batch |
LMS Courseware (encoach_lms_api — courseware.py)
| Method | Route | Description |
|---|---|---|
| GET/POST | /api/courses/<id>/chapters |
List / create chapters for a course |
| GET/PUT/DELETE | /api/chapters/<id> |
Get / update / delete a chapter |
| POST | /api/chapters/<id>/materials |
Upload material (JSON or FormData) |
| GET/PUT/DELETE | /api/materials/<id> |
Get / update / delete a material |
| GET | /api/materials/<id>/download |
Download material file |
| GET | /api/materials/<id>/content |
Serve material file inline (for iframe embed, accepts ?token= param) |
| POST | /api/materials/<id>/view |
Mark material as viewed |
| POST | /api/chapters/<id>/progress/complete |
Mark chapter as completed |
| GET | /api/courses/<id>/completion |
Get course completion status (% progress, post-test availability) |
| GET | /api/materials/search |
Search all materials across courses (for Admin Resources page) |
Institutional (encoach_lms_api)
These power the 14 institutional admin pages under /admin/* (see §14) and back the write-flow test in §17. Every route is JWT-protected.
Academic & Departments
| Method | Route | Description |
|---|---|---|
| GET / POST | /api/academic-years |
List / create academic years |
| GET / PATCH / DELETE | /api/academic-years/<yid> |
Get / update / delete year (DELETE cascades terms) |
| POST | /api/academic-years/<yid>/generate-terms |
Auto-create terms spanning the year (two_sem, three_trim, four_qtr) |
| GET / POST | /api/academic-terms |
List / create academic terms |
| GET / PATCH / DELETE | /api/academic-terms/<tid> |
Term CRUD |
| GET / POST | /api/departments |
List / create departments |
| GET / PATCH / DELETE | /api/departments/<did> |
Department CRUD |
Admissions
| Method | Route | Description |
|---|---|---|
| GET / POST | /api/admission-registers |
List / create admission registers |
| GET / PATCH / DELETE | /api/admission-registers/<rid> |
Register CRUD (DELETE supported) |
| POST | /api/admission-registers/<rid>/confirm |
Confirm register |
| GET / POST | /api/admissions |
List / create admissions (application_date must fall inside register window) |
| GET / PATCH / DELETE | /api/admissions/<aid> |
Admission CRUD (DELETE supported) |
Institutional Exams & Marksheets
| Method | Route | Description |
|---|---|---|
| GET / POST | /api/inst-exam-sessions |
List / create exam sessions |
| GET / PATCH / DELETE | /api/inst-exam-sessions/<sid> |
Session CRUD |
| POST | /api/inst-exam-sessions/<sid>/schedule |
Schedule session |
| GET / POST | /api/result-templates |
List / create result templates |
| GET / PATCH / DELETE | /api/result-templates/<tid> |
Template CRUD (DELETE cascades op.marksheet.register) |
| POST | /api/result-templates/<tid>/generate (alias: /generate-marksheets) |
Generate marksheet registers (tries generate_result then action_generate_marksheet) |
| GET | /api/marksheets |
List marksheet registers |
| POST | /api/marksheets/<mid>/validate |
Validate marksheet |
Facilities, Assets, Activities
| Method | Route | Description |
|---|---|---|
| GET / POST | /api/facilities |
List / create facilities |
| GET / PATCH / DELETE | /api/facilities/<fid> |
Facility CRUD |
| GET / POST | /api/assets |
List / create assets (backed by custom encoach.asset model) |
| GET / PATCH / DELETE | /api/assets/<aid> |
Asset CRUD |
| GET / POST / PATCH / DELETE | /api/activity-types + /<id> |
Activity-type CRUD |
| GET / POST / PATCH / DELETE | /api/activities + /<id> |
Activity CRUD |
Library, Lessons, Gradebook, Leaves, Fees
| Method | Route | Description |
|---|---|---|
| GET / POST | /api/library/media |
List / create media items (authors split into author_ids) |
| GET / PATCH / DELETE | /api/library/media/<mid> |
Media CRUD — detail endpoint returns author_names + author_ids |
| GET / POST | /api/lessons |
List / create lessons |
| GET / PATCH / DELETE | /api/lessons/<lid> |
Lesson CRUD — detail endpoint preserves course_id / batch_id on edits |
| GET / POST / DELETE | /api/grading-assignments + /<id> |
Grading assignment CRUD (auto-resolves assignment_type and default faculty_id) |
| GET | /api/gradebooks + /api/gradebook-lines?gradebook_id=<id> |
Gradebook drill-down |
| GET | /api/student-progress + /<id> |
Student progress list/detail |
| GET / POST | /api/student-leaves |
List / create leave requests |
| POST | /api/student-leaves/<lid>/approve |
Approve leave |
| DELETE | /api/student-leaves/<lid> |
Delete leave |
| GET / POST | /api/fees-plans |
List / create fee plans |
| POST | /api/fees-plans/<fid>/create-invoice |
Generate accounting invoice (auto-wires income account on the product template for Odoo 19 JSONB property storage if missing) |
| POST | /api/fees-plans/<fid>/register-payment |
Register a payment against the invoice |
Other
| Route | Description |
|---|---|
/api/taxonomy/* |
Subject/topic taxonomy tree |
/api/adaptive/* |
Adaptive learning dashboard/signals |
/api/placement/* |
Placement test start/answer/results |
/api/grading/* |
Grading queue, AI suggest |
/api/scores/* |
Score release/reject |
/api/course/* |
Course CRUD, gap analysis, auto-generate |
/api/ai-course/* |
AI course creation pipelines |
/api/auth/register |
User registration |
8. Key Odoo Models
| Model | Table | Description |
|---|---|---|
encoach.exam.custom |
encoach_exam_custom |
Custom exams (16 records) |
encoach.exam.custom.section |
encoach_exam_custom_section |
Exam sections (reading/listening/writing/speaking) |
encoach.question |
encoach_question |
Questions (38 records) |
encoach.student.attempt |
encoach_student_attempt |
Student exam attempts (10 records) |
encoach.student.answer |
encoach_student_answer |
Individual answers |
encoach.entity |
encoach_entity |
Organizations (1: "EnCoach Demo Academy") |
encoach.rubric |
encoach_rubric |
Grading rubrics |
encoach.exam.template |
encoach_exam_template |
Exam templates |
encoach.exam.structure |
encoach_exam_structure |
Exam structures |
encoach.exam.schedule |
encoach_exam_schedule |
Exam schedules |
encoach.exam.assignment |
encoach_exam_assignment |
Exam assignments |
encoach.course.chapter |
encoach_course_chapter |
Course chapters (ordered by sequence) |
encoach.chapter.material |
encoach_chapter_material |
Materials within chapters (pdf, video, article, link) |
encoach.chapter.progress |
encoach_chapter_progress |
Per-student chapter progress tracking |
encoach.course.completion |
encoach_course_completion |
Per-student course completion (% progress, post-test flag) |
encoach.resource |
encoach_resource |
Learning resources linked to taxonomy (subject, domain, topics, objectives) |
encoach.resource.tag |
encoach_resource_tag |
Tags for categorizing resources |
encoach.subject |
encoach_subject |
Taxonomy subjects (e.g. English, Mathematics) |
encoach.domain |
encoach_domain |
Taxonomy domains under subjects (e.g. Grammar, Listening) |
encoach.topic |
encoach_topic |
Taxonomy topics under domains |
encoach.learning.objective |
encoach_learning_objective |
Learning objectives under topics (Bloom levels) |
op.course (extended) |
op_course |
OpenEduCat course + description, max_capacity, encoach_subject_id, topic_ids, learning_objective_ids |
op.student |
op_student |
OpenEduCat student model |
op.faculty |
op_faculty |
OpenEduCat teacher/faculty model |
op.batch |
op_batch |
OpenEduCat batch model |
Institutional models (OpenEduCat + EnCoach)
| Model | Table | Description |
|---|---|---|
op.academic.year |
op_academic_year |
Academic year (name unique, term_structure) |
op.academic.term |
op_academic_term |
Term under a year; FK to year is RESTRICT — delete-year cascades terms first |
op.department |
op_department |
Department |
op.admission.register |
op_admission_register |
Admission register window (start_date / end_date) |
op.admission |
op_admission |
Admission — constrained so application_date ∈ [register.start_date, register.end_date] |
op.exam.session |
op_exam_session |
Institutional exam session |
op.result.template |
op_result_template |
Marksheet result template — generate_result() in Odoo 19 (older: action_generate_marksheet) produces op.marksheet.register rows |
op.marksheet.register |
op_marksheet_register |
Generated marksheet register (lines + validate workflow) |
op.facility |
op_facility |
Physical facility (rooms, labs, etc.) |
encoach.asset |
encoach_asset |
New lightweight asset model (replaces missing op.asset): name, code, facility_id, description, active |
op.activity.type / op.activity |
op_activity_type / op_activity |
Activity categorization + activities |
op.media / op.author |
op_media / op_author |
Library media + authors (M2M) |
encoach.lesson |
encoach_lesson |
Lesson tied to course + batch |
grading.assignment / grading.assignment.type |
grading_assignment* |
Grading assignments — assignment_type and faculty_id are required |
encoach.student.leave |
encoach_student_leave |
Student leave requests with approve workflow |
op.fees.terms + .line |
op_fees_terms* |
Fees terms; line_ids must sum to 100% |
op.student.fees.details |
op_student_fees_details |
Per-student fees; get_invoice() requires an income account on product template (stored as JSONB in Odoo 19) |
9. Installed EnCoach Modules
| Module | Description |
|---|---|
encoach_adaptive |
Adaptive learning engine |
encoach_ai |
AI generation, search, insights |
encoach_ai_course |
AI course creation |
encoach_api |
REST API auth + base helpers |
encoach_branding |
Entity branding |
encoach_core |
Core models (user, entity, role) |
encoach_course_gen |
Course generation |
encoach_exam_template |
Exam CRUD, sessions, rubrics, assignments |
encoach_lms_api |
LMS: courses, students, teachers, batches, chapters, materials, enrollment, progress |
encoach_quality_gate |
Quality gate checks |
encoach_resources |
Learning resources |
encoach_scoring |
Grading and score release |
encoach_taxonomy |
Subject/topic taxonomy |
encoach_vector |
Vector embeddings |
10. Exam Lifecycle Flow
1. Admin → /admin/generation page
├── Select modules (reading, listening, writing, speaking)
├── Set parameters (difficulty, timer, grading, rubric, entity, etc.)
└── Click "Generate with AI" → POST /api/exam/<module>/generate
2. AI generates content (or fallback templates if OpenAI not configured)
3. Admin reviews generated content → Submit
├── "Submit & skip approval" → POST /api/exam/generation/submit (skip_approval=true)
├── "Submit for approval" → POST /api/exam/generation/submit (skip_approval=false)
└── Creates: exam_custom + sections + questions in DB
4. Admin assigns exam → /api/exam/custom/<id>/assign or /api/exam-schedules
5. Student → /student/my-exams → clicks exam → /student/exam/<id>
├── GET /api/exam/<id>/session → loads sections, questions, passage_text
├── POST /api/exam/<id>/autosave → saves answers periodically
└── POST /api/exam/<id>/submit → auto-scores objective questions
6. Student → /student/exam/<id>/results → views score breakdown
11. Student Learning Lifecycle Flow
1. Admin/Teacher → /teacher/courses → "New Course"
├── Create course (title, code, description, max_capacity)
└── Course stored in op.course (extended with description, max_capacity)
2. Teacher → /teacher/courses/:courseId/chapters → "Add Chapter"
├── Create chapters with sequence ordering
└── Upload materials (pdf, video, article, link) per chapter
└── POST /api/chapters/<id>/materials (JSON or FormData)
3. Admin → /admin/courses → "Enroll Students"
├── Bulk-enroll students via POST /api/courses/<id>/enroll
└── Or enroll individually via POST /api/students/<id>/enroll
4. Student → /student/dashboard → sees enrolled courses with progress
├── GET /api/student/my-courses → returns courses + chapter_count + progress %
└── Cards show "Start Learning" or "Continue Learning"
5. Student → /student/courses/:id → course detail with chapter list
├── Shows chapter list with material counts and lock status
└── Student clicks a chapter → /student/courses/:courseId/chapters/:chapterId
6. Student → chapter view → views materials
├── POST /api/materials/<id>/view → marks material as viewed
├── Progress tracked in encoach.chapter.progress
└── When all materials viewed → chapter auto-completes
7. Student → clicks "Mark Chapter Complete"
├── POST /api/chapters/<id>/progress/complete
└── Triggers _check_course_completion()
8. Course completion check
├── When all chapters completed → encoach.course.completion updated
├── progress = 100%, completed_at = now
└── post_test_available = True → student can take final exam
12. Taxonomy ↔ Resource ↔ Course Relationships
encoach.subject (e.g. English)
└── encoach.domain (e.g. Grammar, Listening, Reading)
└── encoach.topic (e.g. Verb Tenses, Relative Clauses)
└── encoach.learning.objective (Bloom levels: Remember, Understand, Apply...)
encoach.resource ──M2O──→ encoach.subject (subject_id)
──M2O──→ encoach.domain (domain_id)
──M2M──→ encoach.topic (topic_ids)
──M2M──→ encoach.learning.objective (learning_objective_ids)
op.course (extended) ──M2O──→ encoach.subject (encoach_subject_id)
──M2M──→ encoach.topic (topic_ids)
──M2M──→ encoach.learning.objective (learning_objective_ids)
encoach.course.chapter ──M2O──→ encoach.subject (encoach_subject_id)
──M2M──→ encoach.topic (topic_ids)
──M2M──→ encoach.learning.objective (learning_objective_ids)
Frontend: TaxonomyCascade Component
Reusable cascading picker (frontend/src/components/TaxonomyCascade.tsx) used in:
- Admin Resource Manager (upload + edit dialogs)
- Teacher Library (upload dialog)
Provides Subject → Domain → Topics → Learning Objectives selection with auto-fetching and reset-on-parent-change logic.
API endpoints for taxonomy data
| Method | Route | Description |
|---|---|---|
| GET | /api/subjects |
List subjects (paginated, returns items array) |
| GET | /api/domains?subject_id=N |
List domains filtered by subject |
| GET | /api/topics?domain_id=N |
List topics filtered by domain |
| GET | /api/learning-objectives?topic_id=N |
List objectives filtered by topic |
13. Known Issues & Notes
- PostgreSQL version: pgdata is PG 18; must use micromamba
pg_ctl(/Users/yamenahmad/micromamba/envs/odoo19/bin/pg_ctl), NOT conda env PG 17. odoo.confrequiresdb_name: Withoutdb_name = encoach_v2, all API routes return 404.npmnot in default PATH: Must add/Users/yamenahmad/.local/node/binto PATH before running frontend.- OpenAI not configured: AI generation uses fallback templates (static mock content). Set OpenAI API key in Odoo system parameters to enable real AI.
- Gitea deployment hook: Frontend v3 server has a
docker-compose.ymlconflict that blocks auto-deploy. SSH and stash/checkout on server to fix. - Student passwords were reset: Student demo users (
sarah,omar,layla) now use passwordstudent123(notadmin). Passwords were reset viapsycopg2+passlibscript. To reset again:from passlib.context import CryptContext ctx = CryptContext(schemes=['pbkdf2_sha512']) new_hash = ctx.hash('student123') # UPDATE res_users SET password = '<new_hash>' WHERE login = 'sarah@encoach.test'; op.courseextended fields: Theop.coursemodel (OpenEduCat) was extended viaencoach_lms_api/models/course_ext.pyto adddescription(Text) andmax_capacity(Integer). If the module is not installed/updated, course creation will fail withValueError: Invalid field 'description'.material_countcomputed field: Thematerial_countonencoach.course.chapterrequires@api.depends('material_ids')to recompute when materials are added. If counts show 0 after adding materials, restart Odoo with-u encoach_lms_api.- Taxonomy API response format: Taxonomy list endpoints (
/api/subjects,/api/domains,/api/topics) return paginated responses with data under anitemskey ({"items": [...], "total": N}), not adatakey. The frontendtaxonomy.service.tshandles both formats. - Resource taxonomy fields via FormData: When uploading resources,
topic_idsandlearning_objective_idsare sent as comma-separated strings in multipart FormData. The backendcreate_resourcehandler parses these into proper Many2many commands[(6, 0, ids)]. - Module upgrades for schema changes: After adding new fields to Odoo models (e.g.
encoach_subject_idonop.course), you must run./run.sh -u <module_name> --stop-after-initto apply DB schema changes before the server can use them.
14. Frontend Key Pages
Admin Pages
| Route | Component | Description |
|---|---|---|
/admin/dashboard |
AdminLmsDashboard | Platform overview, AI tips, stats (LMS-focused) |
/admin/platform |
AdminDashboard | Alternative platform dashboard |
/admin/courses |
AdminCourses | Course table with enroll/assign/delete actions |
/admin/students |
AdminStudents | Student management |
/admin/teachers |
AdminTeachers | Teacher management |
/admin/users |
UsersPage | All platform users + roles (Management section) |
/admin/entities |
EntitiesPage | Entity/organization management |
/admin/classrooms |
ClassroomsPage | Batches/classrooms with student management |
/admin/user-roles |
UserRoles | User role assignments (⚠ NOT /admin/roles) |
/admin/roles-permissions |
RolesPermissions | Roles & permissions matrix |
/admin/authority-matrix |
AuthorityMatrix | Authority matrix across roles |
/admin/generation |
GenerationPage | AI exam generation |
/admin/examsList |
ExamsListPage | List all exams (⚠ NOT /admin/exams) |
/admin/rubrics |
RubricsPage | Manage rubrics |
/admin/assignments |
AssignmentsPage | Manage assignments |
/admin/approval-workflows |
ApprovalWorkflowsPage | Approval workflow config |
/admin/taxonomy |
TaxonomyManager | Manage subjects, domains, topics, objectives (inline editable) |
/admin/resources |
ResourceManager | Upload/edit resources with full taxonomy cascade (Subject→Domain→Topics→Objectives) |
Admin — Institutional Pages
Backed by encoach_lms_api + OpenEduCat models. All 14 pages verified CREATE / EDIT / DELETE / WORKFLOW in the write-flow sweep (see §17).
| Route | Page | Backing API | Key actions |
|---|---|---|---|
/admin/academic-years |
Academic Years | /api/academic-years, /generate-terms |
CREATE, generate-terms, EDIT, DELETE (cascades terms) |
/admin/academic-terms |
Academic Terms | /api/academic-terms |
CRUD |
/admin/departments |
Departments | /api/departments |
CRUD |
/admin/admission-registers |
Admission Registers | /api/admission-registers |
CREATE, confirm, DELETE |
/admin/admissions |
Admissions | /api/admissions |
CREATE (date-constrained), DELETE |
/admin/inst-exam-sessions |
Institutional Exam Sessions | /api/inst-exam-sessions |
CREATE, schedule, EDIT, DELETE |
/admin/marksheets |
Marksheets | /api/result-templates, /api/marksheets |
Create template, generate, validate marksheet, DELETE |
/admin/facilities |
Facilities | /api/facilities, /api/assets |
Facility CRUD + nested assets |
/admin/activities |
Activities | /api/activity-types, /api/activities |
Type + activity CRUD |
/admin/library |
Library | /api/library/media |
Media + authors, detail exposes author links |
/admin/lessons |
Lessons | /api/lessons |
CRUD; edits preserve course_id / batch_id |
/admin/gradebook |
Gradebook | /api/grading-assignments, /api/gradebooks, /api/gradebook-lines |
Assignment CRUD + gradebook drill |
/admin/student-progress |
Student Progress | /api/student-progress |
Drill-through detail |
/admin/student-leave |
Student Leave | /api/student-leaves |
CREATE, approve, DELETE |
/admin/fees |
Fees | /api/fees-plans, /create-invoice, /register-payment |
List seeded plans, generate invoice, register payment |
Teacher Pages
| Route | Component | Description |
|---|---|---|
/teacher/dashboard |
TeacherDashboard | Teacher overview |
/teacher/courses |
TeacherCourses | Course cards with Edit, Chapters & Materials, AI Workbench |
/teacher/courses/new |
CourseBuilder | Create new course |
/teacher/courses/:courseId/chapters |
CourseChapters | Manage chapters and materials for a course |
/teacher/courses/:courseId/chapters/:chapterId |
ChapterDetail | Chapter detail with material list |
/teacher/courses/:courseId/workbench |
AiWorkbench | AI content generation workbench |
/teacher/assignments |
TeacherAssignments | Manage assignments |
/teacher/library |
TeacherLibrary | Upload resources with full taxonomy cascade (Subject→Domain→Topics→Objectives) |
Student Pages
| Route | Component | Description |
|---|---|---|
/student/dashboard |
StudentDashboard | Enrolled courses, progress, grades, quick actions |
/student/courses |
StudentCourses | Enrolled courses with progress badges |
/student/courses/:id |
StudentCourseDetail | Course chapters, materials, grades |
/student/courses/:courseId/chapters/:chapterId |
StudentChapterView | View materials, mark viewed/complete |
/student/my-exams |
StudentExamsPage | Student's assigned exams |
/student/exam/:id |
ExamSession | Take an exam |
/student/exam/:id/results |
ExamResults | View results |
/student/assignments |
StudentAssignments | Homework assignments |
15. Database Connection
Host: localhost
Port: 5432
Database: encoach_v2
User: yamenahmad
Password: (none)
Quick access:
PGBIN="/Users/yamenahmad/micromamba/envs/odoo19/bin"
"$PGBIN/psql" -U yamenahmad -d encoach_v2
16. QA Test Report — 2026-04-18
A full end-to-end browser test was executed across all three user types (admin, teacher, student), driven by an autonomous browser agent. 32 distinct pages were verified plus an exam session flow.
16.1 Backend API Health — 32/33 endpoints healthy (pre-fix)
All tested with admin JWT unless noted:
| Category | Endpoints Tested | Status |
|---|---|---|
| Auth | /api/login, /api/user |
✅ |
| Users & Roles | /api/users/list, /api/users/with-roles, /api/roles, /api/permissions, /api/authority-matrix, /api/user-authority-matrix |
✅ |
| Entities | /api/entities, /api/entities/<id>/roles |
✅ |
| LMS Core | /api/courses, /api/students, /api/teachers, /api/batches, /api/batches/<id>/students |
✅ |
| Taxonomy | /api/subjects, /api/domains, /api/topics, /api/learning-objectives |
✅ |
| Resources | /api/resources, /api/resource-tags |
✅ |
| Exams | /api/exam/custom/list, /api/exam/templates, /api/exam-structures, /api/exam-schedules, /api/rubrics, /api/rubric-groups |
✅ |
| Assignments | /api/assignments |
✅ |
| Stats | /api/stats, /api/stats/performance, /api/analytics/student, /api/adaptive/dashboard |
✅ |
| Courseware | /api/courses/<id>/chapters, /api/courses/<id>/completion, /api/materials/search |
✅ |
| Grading | /api/grading/queue |
✅ |
| Student role | /api/student/my-courses, /api/student/my-exams, /api/assignments |
✅ |
| Approvals | /api/approval-workflows, /api/approval-requests, /api/approval-users |
✅ (after fix) |
16.2 Bugs Fixed During This Pass
Bug #1: /api/approval-workflows returned 500
- Cause:
encoach_exam_template/controllers/approval_workflows.pyran raw SQL against tables (encoach_approval_workflow,encoach_approval_request) that are only created by theencoach_approvalmodule — which lives innew_project/and is NOT installed in the activebackend/. - Fix: Added a
_table_exists(cr, name)helper usingto_regclass(). Bothlist_workflowsandlist_requestsnow return{items: [], total: 0}gracefully when the table is absent, matching the frontend's empty-state expectation.
Bug #2: DELETE /api/courses/<id> returned opaque 500 for courses with enrollments
- Cause:
op.coursehas aRESTRICTFK fromop_student_course. Postgres raised a raw violation with no user-friendly handling. - Fix:
delete_courseinencoach_lms_api/controllers/lms_core.pynow (a) detects enrollments first and returns a 409 with a clear message, (b) accepts?force=trueto cascade-delete enrollments and detach linked batches before removing the course.
16.3 Admin Workflows — All Pages Pass ✅
| Page | Route | Result |
|---|---|---|
| Dashboard | /admin/dashboard |
Stats cards, AI tips, platform insights render |
| Users | /admin/users |
7 users shown with roles, filter tabs work |
| Entities | /admin/entities |
Demo Academy entity, 6 users / 5 roles |
| Classrooms | /admin/classrooms |
IELTS 2026 Spring (3 students), student dialog works |
| User Roles | /admin/user-roles |
7 users, 5 available roles, assignments rendered |
| Roles & Permissions | /admin/roles-permissions |
5 roles × 115 permissions × 22 categories |
| Authority Matrix | /admin/authority-matrix |
257 granted cells, 45 % coverage, CSV export present |
| Courses | /admin/courses |
3 courses, enroll-students dialog with BOTH "Individual" and "By Classroom" tabs verified |
| Taxonomy | /admin/taxonomy |
3 subjects (English, Math, IT) with full tree |
| Resources | /admin/resources |
15 items (6 library + 9 course materials), filters & cascade pickers work |
| Exams List | /admin/examsList |
16 custom exams, search filters correctly |
| Generation | /admin/generation |
Loads; Official/Practice tabs work |
| Rubrics | /admin/rubrics |
3 rubrics + groups tab |
| Assignments | /admin/assignments |
2 assignments, filter tabs work |
| Approval Workflows | /admin/approval-workflows |
Empty-state OK, Create dialog opens |
16.4 Teacher Workflows — All Pages Pass ✅
Logged in as khalid@encoach.test (Dr. Khalid). Tested 11 pages; all work:
/teacher/dashboard— 3 Active Courses, 55 Total Students, 0 Pending Grading, 82 % Avg Pass Rate/teacher/courses— 3 course cards with Edit / Chapters / AI Workbench buttons/teacher/students— 3 students with batch info, attendance, AI risk indicators/teacher/library— 15 resources, upload dialog's taxonomy cascade confirmed working/teacher/assignments— empty state handled gracefully/teacher/courses/3/chapters— empty state + "Add Chapter" dialog opens with Name/Description/Start Date/Unlock Mode/teacher/attendance— batch selector + trend chart + AI warning banners/teacher/timetable— weekly grid 08:00–17:00/teacher/discussions//teacher/announcements//teacher/profile— all load cleanly
16.5 Student Workflows — All Pages Pass ✅
Logged in as sarah@encoach.test (password student123). Tested 16 pages + exam session; all work:
/student/dashboard— 67 % overall progress, 3 courses, 4 upcoming exams popup/student/courses— 3 cards with progress (100 %, 100 %, 0 %)/student/courses/1— course detail with "Chapters (1) / Grades (0) / About" tabs + "Take Post-Test"/student/courses/1/chapters/1— 3/3 materials completed (IELTS Listening video, Reading article, Grammar docs)/student/assignments//student/grades//student/attendance//student/timetable— all load with graceful empty or populated states/student/profile— editable name, email, password change/student/discussions//student/announcements//student/messages//student/journey— empty states clean/student/subjects— 3 subjects with "Start Diagnostic" CTAs/student/subject-registration— registration table renders
Exam flow — /student/exam/2/session loaded "IELTS Academic Mock - Spring 2026 Full Test" with 170-minute timer, Listening section, 7 questions, MCQ with 4 radio options, Previous / Flag / Next navigation. Clean and functional.
16.6 Demo Data State (Post-Cleanup)
| Entity | Count | Notes |
|---|---|---|
| Users | 7 | 2 admins, 2 teachers, 3 students |
| Courses | 3 | IELTS Preparation B2, Python Programming Fundamentals, IELTS Listening & Speaking Intensive (junk "tttttt" removed, "test" renamed) |
| Batches | 1 | IELTS 2026 Spring (3 students) |
| Exams (custom) | 16 | published + draft mix |
| Resources | 6 | library resources; +9 course materials |
| Taxonomy subjects | 3 | English (6d/37t), Mathematics (6d/15t), IT (6d/17t) — junk "math" subject removed |
16.7 Route Naming Gotchas
Two frontend routes differ from the pattern a developer might guess:
| Expected | Actual |
|---|---|
/admin/exams |
/admin/examsList |
/admin/roles |
/admin/user-roles |
The Admin Pages table in §14 has been updated with the full correct route list.
16.8 Minor Observations (Non-Blocking)
- React Router v7 future flag warnings in console on every page (
v7_startTransition,v7_relativeSplatPath) — cosmetic only, ignore until v7 migration. - forwardRef warning in Badge component — dev-only, not a runtime error.
/api/scoresby itself returns 404, but its sub-endpoints (/api/scores/releaseetc.) are valid.- Session occasionally expired during long admin test runs, requiring re-login. Token TTL may warrant increasing for QA flows.
17. Institutional Admin Pages — Write-Flow QA — 2026-04-18
Comprehensive API-level CREATE / EDIT / DELETE / WORKFLOW sweep across all 14 institutional admin pages (see §14 "Admin — Institutional Pages"). Driven by test_write_flows.py against real Odoo data seeded by seed_institutional.py.
Final result: SUMMARY PASS=44 FAIL=0 TOTAL=44 ✅
17.1 Scenarios covered
| Page | Actions verified |
|---|---|
| Academic Years | CREATE, generate-terms (3), EDIT, DELETE (cascades terms) |
| Academic Terms | EDIT, DELETE |
| Departments | CREATE, EDIT, DELETE |
| Admission Registers | CREATE, confirm, DELETE |
| Admissions | CREATE (date-constrained to register window), DELETE |
| Inst Exam Sessions | CREATE, schedule, EDIT, DELETE |
| Marksheets | CREATE template, generate-marksheets, validate marksheet, DELETE template (cascades registers) |
| Facilities | CREATE, EDIT, DELETE |
| Assets | CREATE, DELETE (backed by new encoach.asset model) |
| Activities | CREATE type, CREATE activity, DELETE |
| Library | CREATE media + authors, VERIFY author link, DELETE |
| Lessons | CREATE, EDIT (preserves course_id + batch_id), DELETE |
| Gradebook | CREATE grading-assignment, DELETE, drill lines |
| Student Progress | DRILL detail |
| Student Leaves | CREATE, approve, DELETE |
| Fees | LIST seeded, create-invoice ✅ |
17.2 Backend fixes applied
1. encoach.asset — new lightweight model
op.asset does not exist in the installed OpenEduCat set. Added encoach_lms_api/models/asset.py with name, code, facility_id, description, active; registered in __init__.py; CRUD access granted via security/ir.model.access.csv. facilities.py now uses _asset_model() → env['encoach.asset'].
2. Missing DELETE routes added
DELETE /api/admission-registers/<rid>DELETE /api/admissions/<aid>DELETE /api/result-templates/<tid>(cascadesop.marksheet.registerrows created bygenerate_result)DELETE /api/student-leaves/<lid>DELETE /api/academic-years/<yid>now pre-unlinksacademic_term_idsto avoid theop_academic_term.academic_year_idFK RESTRICT violation.
3. Missing detail (GET-one) endpoints added
GET /api/lessons/<lid>— exposes full lesson incl.course_id,batch_idso EDIT can verify those are preserved.GET /api/library/media/<mid>— returnsauthor_ids+author_names(the list endpoint only returns a comma-separatedauthorstring).
4. Admissions CREATE — defaults + validation
create_admission now defaults application_date = fields.Date.today(), builds name from first_name + last_name, and accepts nationality_id or nationality. The Odoo constraint @api.constrains(...) requires application_date ∈ [register.start_date, register.end_date] — the test payload uses 2028-06-15 to sit inside the seeded register window. Added companion PATCH/PUT /api/admissions/<aid> for edits.
5. Grading assignments — required fields resolved
create_grading_assignment no longer sends user_id (invalid on grading.assignment in this OpenEduCat version). Auto-resolves the required fields:
assignment_type: find-or-creategrading.assignment.typewith codeDEFAULTfaculty_id: first availableop.facultyissued_date: defaults tofields.Datetime.now()
6. Marksheets generate — Odoo 19 compatibility
Route aliased as /api/result-templates/<tid>/generate and /generate-marksheets. Controller tries rec.generate_result() (OpenEduCat 19) then falls back to rec.action_generate_marksheet() (older versions).
7. Fees create-invoice — Odoo 19 JSONB properties
OpenEduCat's op.student.fees.details.get_invoice() raised UserError: There is no income account defined for this product. Root cause: Odoo 19 removed ir_property and stores company-dependent fields (like property_account_income_id) as JSONB columns on the model itself — e.g. product_template.property_account_income_id is now jsonb keyed by company_id.
Fix: create_invoice auto-wires a default income account before invoking get_invoice():
if not (fp or {}).get('income'):
AA = request.env['account.account'].sudo()
# Odoo 19: account.account has a M2M `company_ids`.
try:
income_acc = AA.search([
('account_type', '=', 'income'),
('company_ids', 'in', request.env.company.id),
], limit=1)
except Exception:
income_acc = AA.browse()
if not income_acc:
income_acc = AA.search([('account_type', '=', 'income')], limit=1)
if income_acc:
try:
prod_tmpl.with_company(request.env.company).write({
'property_account_income_id': income_acc.id,
})
except Exception:
_logger.exception('auto-wire income account')
The seed_institutional.py fee-product block now performs the same wiring at seed time, so fresh databases don't hit this error.
17.3 Seeding
seed_institutional.py populates the full institutional fixture set:
- 1 academic year + auto-generated terms
- 1 department
- 1 admission register + 1 admission
- 1 exam session + 1 result template → marksheets
- Facilities, activities, library media, lessons, gradebooks, student leaves
- Fees: 3
op.fees.terms(Full, 2-inst 50/50, 3-inst 34/33/33 summing to 100%), 1product.product(IELTS Preparation B2 Fees) with income account wired, 3op.student.fees.detailsrecords
Run:
cd /Users/yamenahmad/projects2026/odoo/odoo19
./run.sh --shell <<< "exec(open('seed_institutional.py').read())"
17.4 Running the write-flow test
cd /Users/yamenahmad/projects2026/odoo/odoo19
python3 test_write_flows.py
The script is idempotent — it uses timestamped names for academic-year/register payloads so repeated runs don't collide on Odoo's unique constraints. Expected output: SUMMARY PASS=44 FAIL=0 TOTAL=44.
17.5 Operational notes for Odoo 19 institutional data
product_template.property_account_income_idis JSONB keyed bycompany_id— write to it with.with_company(company).write({...}), not via the removedir_propertytable.op.admissionhas a constraint thatapplication_datemust fall inside the parentop.admission.register's[start_date, end_date]— front-end forms should default toregister.start_dateto avoid validation errors.op.result.template.generate_result()createsop.marksheet.registerrows; deleting a template without first unlinking those rows will fail. The DELETE endpoint handles this internally.op.academic.year→op.academic.termFK isRESTRICT; always unlink terms before the year.op.fees.terms.line_idsmust sum to 100% (percentages). Seeded terms conform.
§18 Support Admin Pages — Backend + Write-Flow QA — 2026-04-18
18.1 Scope
The Support group in the admin sidebar is 3 pages:
| Admin URL | Component | Before | After (this session) |
|---|---|---|---|
/admin/tickets |
TicketsPage |
Called /api/tickets which did not exist (404 HTML). |
Full backend — encoach.ticket model + /api/tickets CRUD + assignedToUser. |
/admin/payment-record |
PaymentRecordPage |
Hard-coded mock arrays. | Live data from op.student.fees.details + account.move + stubbed /api/paymob-orders. |
/admin/settings-platform |
SettingsPage |
Hard-coded mock arrays. | Codes tab → encoach.code, Packages tab → ir.config_parameter, Grading tab → ir.config_parameter. |
18.2 New backend artefacts
Model — backend/custom_addons/encoach_lms_api/models/ticket.py:
class EncoachTicket(models.Model):
_name = 'encoach.ticket'
_inherit = ['mail.thread']
subject, description, type, status, source, page_context,
reporter_id, assignee_id, entity_id, resolved_at
Registered in models/__init__.py and security/ir.model.access.csv (full CRUD for base.group_user).
Controllers — added three new files, all registered in controllers/__init__.py:
| File | Routes |
|---|---|
controllers/tickets.py |
GET/POST /api/tickets, GET/PATCH/DELETE /api/tickets/<id>, GET /api/tickets/assignedToUser |
controllers/payments.py |
GET /api/payment-records (derived from op.student.fees.details), GET /api/payment-records/invoices (from account.move out_invoice), GET /api/paymob-orders (stub, returns {data:[], message:"Paymob integration not configured."}) |
controllers/platform_settings.py |
GET/POST/DELETE /api/codes, POST /api/codes/generate, GET/POST/PATCH/DELETE /api/packages, GET/PATCH /api/grading-config |
Packages and grading config are persisted in ir.config_parameter under the keys encoach.platform.packages and encoach.platform.grading — no new model table needed.
18.3 Frontend changes
| File | Change |
|---|---|
frontend/src/services/payments.service.ts |
NEW — typed client for /api/payment-records + /api/paymob-orders. |
frontend/src/services/platformSettings.service.ts |
NEW — typed client for codes, packages, grading-config. |
frontend/src/pages/PaymentRecordPage.tsx |
Rewritten: 4 KPI cards + live Payments table + Paymob empty-state + CSV export. Removed all mock arrays. |
frontend/src/pages/SettingsPage.tsx |
Rewritten: Codes tab with real Generate Single / Generate Batch and delete; Packages tab with Add Package dialog + delete; Grading tab with Save + client/server validation. |
frontend/src/pages/TicketsPage.tsx |
Small fix: accept both data.items and data.data from the pagination envelope. |
18.4 Seeding
seed_institutional.py now also seeds:
- 4 tickets covering all 4 statuses (
open,in_progress,resolved) and all main types (bug,feature,support,feedback), with realistic subjects / descriptions /page_context. - 4 registration codes (2 individual student, 2 corporate batch) — one already marked as used to exercise the badge variant.
18.5 Write-flow test — 29/29 PASS
test_support_flows.py exercises:
| Page | Actions verified |
|---|---|
| Tickets | LIST seeded, filter by status + type, text search, CREATE, GET detail, EDIT → in_progress, EDIT → resolved (auto-stamps resolved_at), ASSIGN, assignedToUser listing, DELETE |
| Codes (Settings) | LIST seeded, generate individual ×3, filter used=false, DELETE, generate corporate ×2 |
| Packages (Settings) | LIST defaults, CREATE, EDIT price+discount, DELETE, persistence roundtrip |
| Grading (Settings) | GET, set new scale, server-side validation (rejects min >= max), restore IELTS default |
| Payment Record | LIST + totals, filter paid=false, invoices list (account.move), Paymob stub returns empty |
Run with:
cd /Users/yamenahmad/projects2026/odoo/odoo19
python3 test_support_flows.py
Latest output: SUMMARY PASS=29 FAIL=0 TOTAL=29.
18.6 Browser verification
All 3 pages were opened in a real browser (localhost:8080) after login as admin / admin and visually verified:
- Tickets — 4 seeded rows visible, reporter = "Administrator", Create Ticket dialog submits and a new row appears immediately.
- Payment Record — 4 KPI cards (
Total records,Paid,Unpaid,Total amount), 3 payment rows for the seeded students, Paymob tab shows the empty-state message. - Settings — Codes tab lists all seeded + generated codes with
Generate Singleworking and a success toast; Packages tab shows the 3 default cards; Grading tab shows 0..9 step 0.5 andSavereturns a success toast.
No critical console errors — only React Router v7 future-flag warnings.
18.7 Operational notes
- The
encoach.ticketmodel inheritsmail.threadso OCA-style change tracking works out of the box. Adding followers / comments will work without further wiring. - The
/api/payment-recordsendpoint is derived (not stored) — it reflects whatever is inop.student.fees.details. Creating an invoice from the Fees page will flip thepaidbadge automatically on the Payment Record page once the invoice is paid. /api/paymob-ordersis a deliberate empty stub. When real Paymob integration is wired, replace the stub body incontrollers/payments.py::list_paymob_orders— no UI change required; the Payment Record page will light up automatically.- Packages and grading config live in
ir.config_parameter. To reset to defaults, delete the two parametersencoach.platform.packagesandencoach.platform.grading.
§19 Training Admin Pages — Backend + Write-Flow QA — 2026-04-18
19.1 Scope
The Training group in the admin sidebar is 2 pages:
| Admin URL | Component | Before | After (this session) |
|---|---|---|---|
/admin/training/vocabulary |
VocabularyPage |
Hard-coded vocabItems array (no API). |
Full backend: CRUD + search + level filter + per-user progress tracking. |
/admin/training/grammar |
GrammarPage |
Hard-coded grammarItems array (no API). |
Full backend: CRUD + search + level filter + per-user progress tracking. |
19.2 New backend artefacts
Models — backend/custom_addons/encoach_lms_api/models/training.py:
| Model | Key fields |
|---|---|
encoach.vocab.item |
word (unique), meaning, example_sentence, level (CEFR A1–C2), part_of_speech, category, active, computed learners_count + completion_count |
encoach.vocab.progress |
user_id, vocab_id, completed, mastery (learning/familiar/mastered), last_reviewed, review_count — unique per (user, vocab) |
encoach.grammar.rule |
name (unique), description, example, level, category, active, computed stats |
encoach.grammar.progress |
user_id, rule_id, completed, last_reviewed, review_count — unique per (user, rule) |
All four models are registered in models/__init__.py and security/ir.model.access.csv (full CRUD for base.group_user). Uniqueness is enforced via @api.constrains instead of _sql_constraints — Odoo 19 deprecated the latter (see operational notes below).
Controller — backend/custom_addons/encoach_lms_api/controllers/training.py:
| Method / Path | Verb | Purpose |
|---|---|---|
/api/training/vocabulary |
GET |
List with filters: level, category, active, search, pagination. Returns items, total, and a summary block (total, completed, remaining, completion_rate) relative to the authenticated user. |
/api/training/vocabulary |
POST |
Create (word + meaning required, unique word). |
/api/training/vocabulary/<id> |
GET |
Fetch a single item with the caller's progress merged in. |
/api/training/vocabulary/<id> |
PATCH |
Update any mutable field. |
/api/training/vocabulary/<id> |
DELETE |
Delete (cascades progress). |
/api/training/vocabulary/<id>/progress |
POST |
Mark completed / set mastery for the authenticated user (creates the progress row if absent, increments review_count). |
/api/training/grammar (+ same suffixes) |
— | Analogous five endpoints for grammar rules plus /progress. |
ValidationError and UserError raised from the model layer now surface as HTTP 400 (not 500), so the frontend can show user-friendly toasts.
19.3 Frontend changes
| File | Change |
|---|---|
frontend/src/services/training.service.ts |
NEW — typed client for both vocabulary and grammar (list, CRUD, progress). |
frontend/src/pages/VocabularyPage.tsx |
Rewritten. Now: progress summary card with live % complete, search + level filter, checkable list rows, Add Word dialog with CEFR + part-of-speech, delete button per row. AI panel text is dynamic based on summary.remaining. |
frontend/src/pages/GrammarPage.tsx |
Rewritten. Same pattern as vocabulary but tailored to grammar (rule name, description, example, category). |
19.4 Seeding
seed_institutional.py now seeds:
- 10 vocabulary items spanning B1–C1, with realistic meanings and example sentences (Ubiquitous, Pragmatic, Eloquent, Meticulous, Ambiguous, Coherent, Versatile, Concise, Collaborate, Rationale).
- 8 grammar rules spanning A2–C1 (Conditionals Type 2/3, Passive Voice, Relative Clauses, Subject-Verb Agreement, Modal Speculation, Reported Speech, Articles, Present Perfect vs Past Simple).
- 6 progress rows for the admin user (3 vocab mastered, 3 grammar completed) so the UI always shows non-zero state on a freshly seeded DB.
Run with:
.conda-envs/odoo19/bin/python odoo/odoo-bin shell -c odoo.conf --no-http <<'EOF'
exec(open('seed_institutional.py').read())
EOF
The seeding block is idempotent — re-running it never creates duplicates (each row checks by unique key first).
19.5 Write-flow test — 26/26 PASS
test_training_flows.py exercises:
| Page | Actions verified |
|---|---|
| Vocabulary | LIST seeded, filter by level=B2, search eloqu, CREATE, duplicate-word rejection, missing-meaning rejection, GET detail, EDIT level+meaning, PROGRESS mark completed, summary recomputation after completion, PROGRESS un-mark, DELETE, GET-after-DELETE → 404 |
| Grammar | LIST seeded, filter by level=B1, search passive, CREATE, missing-description rejection, GET detail, EDIT level+category, PROGRESS mark / un-mark, DELETE, GET-after-DELETE → 404 |
| Pagination envelope | size=3 page=1 (vocab), size=3 page=2 (grammar) — confirms page + size are echoed in the response |
Run with:
cd /Users/yamenahmad/projects2026/odoo/odoo19
python3 test_training_flows.py
Latest output: SUMMARY PASS=26 FAIL=0 TOTAL=26.
19.6 Browser verification
Both admin pages were opened in a real browser (localhost:8080) after login as admin / admin:
- Vocabulary — progress card shows live
X/N completed (Y%)with a progress bar; clicking the circle next to a row marks it complete and the summary instantly recomputes; search"elo"narrows the list to just Eloquent; the Add Word dialog creates a new row + shows a toast. - Grammar — all 8 seeded rules visible (A2–C1), progress tracking works identically; Add Rule dialog creates a new row.
No functional errors in the console — only React Router v7 future-flag warnings.
19.7 Operational notes for Odoo 19 training data
_sql_constraintsis deprecated in Odoo 19 (the registry emitsModel attribute '_sql_constraints' is no longer supported). Uniqueness and range checks in these models are therefore implemented with@api.constrains, which continues to work on both Odoo 18 and 19.- Because both progress models have
ondelete='cascade'back to the library item, deleting a vocabulary/grammar row automatically cleans up every learner's progress row — no orphan rows. - Progress rows are keyed to
res.users.id, notop.student. Any authenticated API user (admin, teacher, student) sees only their own completions in thesummaryblock, whilelearners_count/completion_counton the library item itself aggregate across all users. - The
/api/training/<kind>/<id>/progressendpoint is upsert-like: the first call creates the progress row, subsequent calls update it and bumpreview_count. This lets the frontend treat it as "toggle" without worrying about a missing row.
20. Reports Section (2026-04-19)
Before this pass, the three pages under the sidebar "Reports" group were pure mock UIs — hardcoded JavaScript arrays in each component, non-functional filters, no network requests. They now aggregate real data from the attempt pipeline.
20.1 Scope
| Page | URL | Before | After |
|---|---|---|---|
| Student Performance | /admin/student-performance |
6 hardcoded students ("Sarah Johnson", "Ahmed Hassan"…) | Per-student band averages from encoach.student.attempt, CEFR level derived from the latest reportable attempt, entity + level + search filters, CSV export, three KPI cards |
| Stats Corporate | /admin/stats-corporate |
Hardcoded bar/line/pie data | Four tabs (Overview, Trends, Distribution, Comparison) driven by a single /api/reports/stats-corporate rollup — bar chart of module averages (× 10), six-month trend, CEFR distribution pie, and entity comparison bar. Threshold + entity + months filters all hit the backend. |
| Record | /admin/record |
Four hardcoded rows | Paginated per-user attempt history with search / entity / user / period filters, real exam codes (EX-###), duration derived from started_at → completed_at, CSV export |
All three pages share a single /api/reports/filters endpoint that returns the entities and students that actually have attempts, so dropdowns never offer filters that would produce empty results.
20.2 Backend artifacts
New controller — backend/custom_addons/encoach_lms_api/controllers/reports.py:
| Route | Method | Purpose |
|---|---|---|
/api/reports/student-performance |
GET | One row per student. Aggregates per-skill bands across all their reportable attempts, takes the CEFR level from the most recent attempt (or derives it from overall band). Filters: entity_id, level, search, since. |
/api/reports/stats-corporate |
GET | Corporate rollups: by_module (4 module averages × 10 for the bar chart), trend (last N months, default 6), distribution (CEFR bins with palette colors), comparison (per-entity average × 10). Filters: entity_id, threshold (0-100), months, since. |
/api/reports/record |
GET | Paginated attempt history. Same shape as {items, total, page, size}. Filters: user_id, entity_id, period=day|week|month, status, page, size. Emits a normalized status_label and an EX-### exam code. |
/api/reports/filters |
GET | Lightweight picker payload: {entities: [...], students: [...]} — only students that have at least one attempt, only entities that exist. |
An attempt is considered reportable if status ∈ {completed, scoring, scored, released}. In-progress attempts are skipped from aggregates so a student with only a half-finished exam doesn't show up with zeros.
CEFR labels are normalized to canonical display casing (Pre-A1 / A1 / … / C2) via _normalize_cefr, and falls back to band-based bucketing via _cefr_for_band when cefr_level is null on the attempt.
No new models were needed — the Reports section only reads from:
encoach.student.attempt(overall / per-skill bands, cefr_level, entity_id, started/completed timestamps)encoach.exam.custom(exam title — note: this model usestitle, notname, which tripped the first version of the controller)encoach.exam.assignment(optional, to link an attempt back to its assignment when one exists)encoach.entity,res.users
20.3 Frontend artifacts
New service — frontend/src/services/reports.service.ts:
reportsService.studentPerformance({ entity_id, level, search, since })
reportsService.statsCorporate({ entity_id, threshold, months, since })
reportsService.record({ user_id, entity_id, period, status, page, size })
reportsService.filters()
Typed interfaces: StudentPerformanceRow, StatsCorporateResponse (with nested StatsModuleRow / StatsTrendRow / StatsDistributionRow / StatsComparisonRow), RecordRow, ReportsFiltersResponse.
Rewritten pages:
frontend/src/pages/StudentPerformancePage.tsx— dropped all hardcoded arrays; added three KPI cards (students tracked, avg overall, top performer level), a search input, Entity select populated from/api/reports/filters, Level select (all / A1 / A2 / B1 / B2 / C1 / C2), CSV export, loading + empty states. Per-row AI Grade Explainer retained.frontend/src/pages/StatsCorporatePage.tsx— dropped all hardcoded chart data; wired every tab to thestats-corporatepayload. Added loading + empty states. Threshold buttons (0/50/70/90%) now hit the backend. Comparison tab now renders a real entity bar chart instead of a placeholder panel. AI narrative bubbles receive live data.frontend/src/pages/RecordPage.tsx— dropped all hardcoded records; wired filters (entity, user, period) + CSV export. Status badge variant adapts tocompleted / released / scored / in_progress / scoring.
20.4 Seeding — seed_reports.py (idempotent)
The live database had only 12 attempts, most of them in-progress with no band scores, so the Reports pages would have shown empty charts. The new script:
- Ensures three entities exist:
Acme Corp (ACME),Global Ltd (GLOBAL),Tech Co (TECHCO)— usingcode(required NOT NULL onencoach.entity) as the idempotency key. - Walks every existing
in_progressattempt and assigns it a plausible score matrix fromBAND_MATRIX, marks itcompleted, stampscompleted_at = started_at + 120 min, and parks it in one of the three entities if it had none. - Creates up to
6 months × 4 students = 24additional historical attempts (with a random 30% skip to keep the trend line jagged), using a small monthlydeltaso the trend chart rises over time. Each new row is guarded by a 6-day proximity lookup so re-running only fills gaps. - Commits once at the end and prints the final reportable-attempt count.
Result after the first run: 28 reportable attempts spread across 3 entities, 4 students, and 6 months — enough to populate every chart.
Run with:
cd /Users/yamenahmad/projects2026/odoo/odoo19/odoo
../.conda-envs/odoo19/bin/python odoo-bin shell -c ../odoo.conf --no-http --stop-after-init < ../seed_reports.py
20.5 Smoke test — 25/25 PASS
test_reports_flows.py exercises all four endpoints:
| Endpoint | Checks |
|---|---|
/api/reports/filters |
GET returns {entities: [...], students: [...]} with at least one of each |
/api/reports/student-performance |
LIST returns rows, row shape complete, overall numeric, CEFR level is a known label, search filter narrows to the matching student, level filter returns only that level, entity_id filter narrows rows |
/api/reports/stats-corporate |
Envelope has by_module + trend + distribution + comparison + meta; by_module has the 4 IELTS modules; trend length == months (default 6, also tested with 3); distribution has per-level color; threshold=70 drops attempts considered (28 → 14); entity filter narrows comparison; months=3 shortens trend |
/api/reports/record |
Paginated list, row shape complete, exam_code is EX-###, status_label is titlecased, user_id filter scopes to that student only, entity_id filter scopes to that entity, period=month returns the in-window subset, pagination size=5 page=1 + page=2 both work |
Latest output: Summary: 25 passed, 0 failed, 25 total.
Regressions also re-run on this pass: Configuration 24/24, Support 29/29, Training 26/26 — all still green.
20.6 Browser verification
Logged in as admin / admin on localhost:8080 and walked all three pages:
- Student Performance — KPI cards populated (5 students tracked, 6.3 average band), table shows real students (Sarah Ahmed, Omar Khan, Layla Nasser, TestUser WriteFlow, Admin User), Level
A2filter narrowed to 2 rows, Entity dropdown showed real entities, CSV export triggered a download. No console errors. - Stats Corporate — Heading confirmed
(28 scored attempts). Overview bar chart shows 4 modules with values ~64–72. Trends line chart has 6 monthly points Oct–Apr peaking at ~85. Distribution pie: C1 11 / B2 6 / B1 5 / A2 4. Comparison bar: Global Ltd ~81, Acme Corp ~63, Tech Co ~62. AI Summary bubbles quote the real numbers. All filters responsive. - Record — Heading
(28 attempts), table shows real student names / exam codes (EX-001, EX-005, EX-017…) / dates / scores / statuses. User filter scoped the list to that student; CSV export worked.
Network tab showed /api/reports/filters 200, /api/reports/record?size=100 200, /api/reports/student-performance 200, /api/reports/stats-corporate 200 — zero 4xx/5xx.
20.7 Gotchas fixed during this pass
encoach.exam.customusestitle, notname. The first version of the Record endpoint threwAttributeError: 'encoach.exam.custom' object has no attribute 'name'. The controller now usesgetattr(exam, 'title', None) or getattr(exam, 'display_name', '')so it survives an upstream rename.encoach.entityhas aNOT NULLcodecolumn.seed_reports.pyoriginally tried to create entities with just{'name': ...}and hit aNotNullViolation. The seeder now passes(name, code)and also looks up bycodewhen probing for existing rows.- Both
encoach_exam_templateandencoach_scoringdeclareencoach.student.attempt. The Reports controller only reads fields that exist on both definitions (listening/reading/writing/speaking/overall_band,cefr_level,status,started_at, and whichever ofcompleted_at/finished_atis present —_attempt_completed_at()probes both). No field access assumes a specific addon version. - In-progress attempts are excluded from the aggregates. If you ever notice a student you expect to see missing from Student Performance or Stats Corporate, check their attempt status —
in_progressattempts are deliberately skipped. They DO still appear in the Record page (which shows everything) unless you also passstatusorperiodfilters.
§21 — Hardening Release (Phase 2/3 completion)
This section records the final wave of work that closed out all remaining items in the Phase 2 (performance & reliability) and Phase 3 (core AI features, accessibility, onboarding) roadmaps. After this release the platform is considered deployment-ready.
21.1 What shipped
The release is organised by capability, not file path.
Human-in-the-loop AI quality loop (P3.3 → P3.5)
| Stage | Where | What it does |
|---|---|---|
| 1. Quality gate | encoach_exam_template.services.quality_checker + ielts_validator |
Runs after every AI exam submit; if scores fall below thresholds the exam is parked in pending_review instead of published. |
| 2. Review queue | encoach_exam_template/controllers/review_workflow.py + frontend/src/pages/admin/ExamReview{Queue,Detail}.tsx |
Admin-only pages list pending exams, surface aggregate + per-question quality reports, and let a reviewer approve (→ published) or reject (→ draft) with audit notes. Writes to new reviewed_by_id, reviewed_at, review_notes fields. |
| 3. Prompt library | encoach_ai.models.ai_prompt + controllers/prompt_controller.py + frontend/src/pages/admin/AIPromptEditor.tsx |
encoach.ai.prompt with a unique (key, version) key, one active row per key. Authors can add a new version, activate it (auto-deactivates prior), and dry-run render with sample variables. Renderer uses str.format_map against a safe dict. |
| 4. Student feedback | encoach_ai.models.ai_feedback + controllers/feedback_controller.py + frontend/src/components/AIFeedbackButtons.tsx + frontend/src/pages/admin/AIFeedbackTriage.tsx |
Thumbs up/down on any AI artefact (question, coach, explanation, translation, narrative, other). Unique (user, subject_type, subject_id) constraint gives upsert semantics; thumbs-down requires a comment. Admin triage page lists/filters/resolves feedback (`open → acknowledged |
Together these four stages mean every AI output can be gated, reviewed, iterated, and measured — closing the loop from prompt → generation → delivery → feedback → next prompt revision.
Compliance (P3.2)
New encoach_api/controllers/gdpr.py + models/gdpr_erasure.py:
GET /api/gdpr/export— JSON dump of the calling user's profile, entity memberships, exam attempts, answers, AI feedback, AI calls, tickets, coaching sessions.POST /api/gdpr/delete— requires{"confirm": true}; anonymises the partner PII, deletes personal feedback + coaching transcripts, strips free-text PII from retained exam answers, nullsuser_idon AI logs, deactivates theres.usersrow, and writes a tombstone toencoach.gdpr.erasure.requestfor audit. Admin accounts are blocked from self-erasure.frontend/src/pages/PrivacyCenter.tsx— routed at/student/privacy,/teacher/privacy,/admin/privacy; download-my-data button produces a timestamped JSON file; erase flow is gated behind atype "DELETE" to confirmalert dialog.
Paymob real checkout (P2.6)
New encoach_lms_api/controllers/paymob.py + models/paymob_order.py:
POST /api/payments/paymob/checkoutruns the three-step Paymob Accept flow (auth → order → payment key) and returns the hosted iframe URL. Ourencoach.paymob.orderrow is created before the external call so partial failures don't lose orders.POST /api/payments/paymob/webhookverifies the HMAC-SHA512 signature over Paymob's canonical field sequence (amount_cents|created_at|currency|…|success). Mismatches return 401; duplicates are idempotent; unknown orders return 200 to stop Paymob retrying.- Credentials read at request time from
ir.config_parameter(encoach.paymob.api_key,hmac_secret,integration_id,iframe_id) so operators can rotate without restarting Odoo. GET /api/paymob-ordersnow reads real rows instead of returning[].
Internationalisation (P3.1)
frontend/src/i18n/bootstrapsi18next+i18next-browser-languagedetectorwith nested feature-scoped namespaces.- Initial locales:
en(source of truth, typed withTranslationsinterface) andar(full translation of the same keys). - RTL handling: when the active language is in
RTL_LANGS,document.documentElement.diris flipped tortlso Tailwind utilities and Radix primitives adapt. frontend/src/components/LanguageToggle.tsxadded next to the theme toggle in bothAdminLmsLayoutandRoleLayoutheaders.- Loaded via
main.tsxside-effect import so every route picks it up.
CI scaffolding (P3.8)
.github/workflows/ci.ymlwith two jobs:- frontend:
tsc --noEmit→eslint→vite build→ Playwright smoke tests against the Vite preview server; uploads a Playwright HTML report on failure. - backend: spins up Postgres 16 as a service and runs the Odoo 19 container with
--test-enable --test-tags encoach_apiagainst a throwawayencoach_cidatabase.
- frontend:
backend/custom_addons/encoach_api/tests/test_health.py— firstHttpCase: asserts/api/healthreturns 200 and/api/health/readyreturns 200 or 503 (never 5xx).frontend/playwright.config.ts+frontend/e2e/login.spec.ts— the login page renders the sign-in form;/redirects unauthenticated traffic to/login.- New npm scripts:
test:e2e,test:e2e:install.
21.2 New DB tables (summary)
| Table | Purpose | Added in |
|---|---|---|
encoach.ai.prompt |
Versioned prompt templates (key, version, is_active, content). |
P3.4 |
encoach.ai.feedback |
Student thumbs up/down on AI output, with triage status. | P3.5 |
encoach.gdpr.erasure.request |
Tombstone row per right-to-erasure request. | P3.2 |
encoach.paymob.order |
Full lifecycle of every Paymob checkout, including the verified HMAC. | P2.6 |
All have per-group ir.model.access.csv entries: admin read/write, authenticated-user read for metadata, create-only on feedback.
21.3 New REST endpoints
Everything requires a valid JWT unless noted; write routes additionally check base.group_system where relevant.
GET /api/ai/prompts list keys (latest per key)
GET /api/ai/prompts/:key/versions full history for a key
GET /api/ai/prompts/:id one version (full content)
POST /api/ai/prompts create new version (admin)
POST /api/ai/prompts/:id/activate flip active pointer (admin)
POST /api/ai/prompts/:id/render dry-run render with sample vars
POST /api/ai/feedback upsert (user)
GET /api/ai/feedback/summary up/down counts + my rating
GET /api/ai/feedback admin triage list
POST /api/ai/feedback/:id/resolve admin triage resolve
GET /api/gdpr/export full data-subject export (self)
POST /api/gdpr/delete right-to-erasure (self, confirm=true)
POST /api/payments/paymob/checkout start a checkout
POST /api/payments/paymob/webhook HMAC-verified callback (public)
GET /api/payments/paymob/orders list my orders
GET /api/paymob-orders admin + self order list (rewired)
21.4 Frontend surface (new routes)
/admin/exam/review-queue → ExamReviewQueue.tsx
/admin/exam/review/:examId → ExamReviewDetail.tsx
/admin/ai/prompts → AIPromptEditor.tsx
/admin/ai/feedback → AIFeedbackTriage.tsx
/student|teacher|admin/privacy → PrivacyCenter.tsx
Plus the AIFeedbackButtons primitive (drop-in <ThumbsUp/>/<ThumbsDown/> that handles the upsert, dialog, and comment prompt) and LanguageToggle in both main layouts.
21.5 Verification
python3 -m compileall -q backend/custom_addons/encoach_ai backend/custom_addons/encoach_api backend/custom_addons/encoach_lms_api— clean.npx tsc --noEmit -p tsconfig.app.json— clean.npm run build— succeeds; bundle analyser shows the same split profile as the §20 release (initial JS ≈ 250 KB gzipped on the login path); new features are lazy-loaded chunks.- New modules do not depend on optional addons: all cross-model reads use
if model in request.envguards so missing addons degrade gracefully instead of 500-ing.
21.6 Configuration operators need to set
Before the Paymob flow will actually charge anything, set in Settings → Technical → System Parameters:
encoach.paymob.api_key = <merchant API key>
encoach.paymob.hmac_secret = <from Paymob dashboard>
encoach.paymob.integration_id = <default integration (card)>
encoach.paymob.iframe_id = <hosted iframe id>
Until those are set, POST /api/payments/paymob/checkout returns 503 with a descriptive message instead of silently crashing.
21.7 Deferred (known)
P1.2— blanket-sudo audit + entity-isolationir.rules. Needs a coordinated frontend + data migration pass; deferred to a follow-up release. Current security model relies on JWT identity + controller-levelhas_groupchecks.- Broader i18n coverage. Only the keys listed in
src/i18n/locales/en.tsare translated today — extending to every page is a rolling job that can happen in small PRs now that the plumbing is in place. - Playwright coverage is intentionally minimal (login + redirect); it exists as a safety net, not as full end-to-end coverage.
22. LangGraph as Core AI Runtime (2026-04-25)
Before this pass every AI feature on the platform talked to OpenAI through hand-rolled service classes (OpenAIService, CoursePlanPipeline, ad-hoc prompts in controllers). That worked but it had no first-class concept of agents, no way to bind tools, no review/revise loop, no place to swap models per use case, and no admin surface to configure any of it. The user asked us to make LangGraph the foundation for every AI feature and to turn /admin/ai/prompts into a real AI Agents & Tools configuration console with sensible defaults shipped on day one.
22.1 What this section delivers
| Layer | Before | After |
|---|---|---|
| Modelling | Prompts only (encoach.ai.prompt) |
Prompts + encoach.ai.agent + encoach.ai.tool (M2M) |
| Runtime | Direct openai.ChatCompletion calls |
LangGraph StateGraph compiled per agent (4 topologies) |
| Tooling | None | 11-tool registry wrapping existing services |
| Config UI | Single prompt editor | Tabbed Agents / Tools / Prompts console with config dialog + Test Runner |
| Pipelines | CoursePlanPipeline was hardcoded |
Routes through AgentRuntime when encoach_ai.use_langgraph_runtime is on (default True) |
| Defaults | None | 7 pre-seeded agents covering every product pillar |
| i18n | EN only | EN + AR (RTL) for every new key |
22.2 New backend artefacts
Models (backend/custom_addons/encoach_ai/models/ai_agent.py)
encoach.ai.agent
key (Char, unique) # stable slug used by callers
name, description (Char/Text)
active (Boolean, default True)
model (Char) # primary OpenAI model
fallback_model (Char) # auto-tried on rate-limit / 5xx
temperature (Float, 0..2)
max_tokens (Integer)
response_format (Selection: text | json)
graph_type (Selection: simple | plan_review_revise | rag | react)
max_revisions (Integer) # cap for review→revise loop
quality_checks (Char) # comma-separated tool keys to run
prompt_key (Char) # link to encoach.ai.prompt
system_prompt (Text) # overrides prompt lookup if set
tool_ids (M2M to encoach.ai.tool)
encoach.ai.tool
key (Char, unique) # e.g. `resources.search`
name, description (Char/Text)
category (Selection: retrieval | reference | quality | persistence | scoring | custom)
schema_json (Text) # JSON-schema for arguments
mutates (Boolean) # writes to DB? (UI flags it)
active (Boolean)
sequence (Integer)
Access rules added in security/ir.model.access.csv for both models — base.group_system write, authenticated read for agents (so the LMS can list which agents exist) and admin-only read for tools.
Services
-
services/agent_tools.py— handler registry. A@register("tool.key")decorator binds a Python callable to a tool key;invoke(env, key, params)is the only entry point and it returns{"ok": bool, ...}. The 11 default handlers wrap existing services so the agent layer doesn't duplicate logic:Tool key Wraps Mutates? resources.searchencoach_vectorsemantic search over LMS resourcesno rubric.fetchencoach.rubriclookup by id or skillno outcomes.fetchencoach.learning.objectivelookup by course / CEFRno student.profileencoach.user.gaprollup → CEFR + gap_jsonno quality.cefr_checktextstatFlesch-Kincaid → CEFR windowno quality.ai_detectGPTZero ( encoach_ai_detect.service)no quality.content_gateencoach_quality_gate.gate.run()no course_plan.saveencoach.course.plancreate +weekslinesyes course_plan.save_materialsencoach.course.plan.materialcreateyes scoring.grade_writingencoach_scoring.writing_examinerno scoring.grade_speakingencoach_scoring.speaking_examinerno -
services/agent_runtime.py—AgentRuntimecompiles alanggraph.graph.StateGraphper agent. The compiled graph is cached per(agent.key, agent.write_date)so config edits invalidate the cache automatically. Four topologies are supported:graph_typeNodes Use case simpleprepare → llm → finalizeOne-shot grading / classification (e.g. writing_grader,speaking_grader).plan_review_reviseprepare → llm → quality_check → (revise → llm)* → finalizeGeneration that needs a quality gate + bounded revision loop (course plans, week materials, exam generation). ragretrieve (resources.search) → prepare → llm → finalizeGeneration that must ground itself in approved library content first. reactLangGraph ToolNode+ agent loop withtool_callsConversational tool-using agents (LMS tutor, personalised exercise generator). The runtime emits a structured trace alongside the output:
{"output": ..., "tool_calls": [...], "retrieval_hits": [...], "revisions": [...], "quality_issues": [...], "model_used": ..., "ms": ..., "fallback_used": bool}. The Test Runner UI renders that trace verbatim. -
Fallback / resilience — Each
llmnode catchesopenai.RateLimitError/openai.APIError/openai.APIConnectionErrorand retries once onagent.fallback_model. The graph state carries_attemptso retries don't loop forever.
Controllers (controllers/agents_controller.py)
GET /api/ai/agents list (admin)
GET /api/ai/agents/<key> one agent + bound tools (admin)
PUT /api/ai/agents/<key> update model/temp/graph/system_prompt/tools (admin)
POST /api/ai/agents/<key>/test run with sample input → returns trace
GET /api/ai/agents/<key>/tools tools currently bound
POST /api/ai/agents/<key>/tools/<key>/toggle bind/unbind one tool
GET /api/ai/tools tool registry (admin)
GET /api/ai/tools/<key> one tool descriptor
PUT /api/ai/tools/<key> edit description/schema/active (admin)
Every write route checks base.group_system; reads check JWT only.
Default seed (data/agents_defaults.xml, noupdate=True)
Seven default agents — one per product pillar — wired to the right tools out of the box:
| Key | Topology | Model / fallback | Temp | Tools bound | Used by |
|---|---|---|---|---|---|
course_planner |
plan_review_revise | gpt-4o / gpt-4o-mini | 0.4 | outcomes.fetch, resources.search, quality.cefr_check, course_plan.save | Smart Wizard, /api/ai/course-plan |
course_week_materials |
plan_review_revise | gpt-4o / gpt-4o-mini | 0.6 | outcomes.fetch, resources.search, quality.cefr_check, course_plan.save_materials | Week-N material generator |
exam_generator |
plan_review_revise | gpt-4o / gpt-4o-mini | 0.5 | resources.search, outcomes.fetch, rubric.fetch, quality.cefr_check | Exam generation pipeline |
exercise_generator |
react | gpt-4o-mini / gpt-4o | 0.7 | student.profile, resources.search, outcomes.fetch, quality.cefr_check | Personalised practice |
lms_tutor |
react | gpt-4o-mini / gpt-4o | 0.6 | resources.search, student.profile, outcomes.fetch | LMS chat |
writing_grader |
simple | gpt-4o / gpt-4o-mini | 0.2 | rubric.fetch, scoring.grade_writing | Writing submissions |
speaking_grader |
simple | gpt-4o / gpt-4o-mini | 0.2 | rubric.fetch, scoring.grade_speaking | Speaking submissions |
__manifest__.py adds langgraph>=0.2.0 and langchain-core>=0.3.0 to external_dependencies; requirements.txt mirrors the same pins.
A feature-flag system parameter is also seeded:
encoach_ai.use_langgraph_runtime = True
Flip it to False (Settings → Technical → System Parameters) to bypass LangGraph and use the legacy SDK path — useful for incident response.
Pipeline rewiring
backend/custom_addons/encoach_ai_course/services/course_plan_pipeline.py now consults the feature flag. When on, both generate_plan and generate_week_materials route through AgentRuntime.run_agent("course_planner", …) / ("course_week_materials", …). The legacy hand-rolled OpenAI path is kept as the else-branch so we can fall back instantly without redeploying.
22.3 Frontend artefacts
- Types —
frontend/src/types/aiAgent.tsdefinesAIAgent,AITool,AgentTestResult,GraphType, etc. - Service —
frontend/src/services/aiAgent.service.tswraps every/api/ai/agents*and/api/ai/tools*route with React Query-friendly helpers. - Page —
frontend/src/pages/admin/AIPromptEditor.tsxis now a tabbed shell:- Agents (
AIAgentsPanel.tsx) — card grid of every agent with badges for graph type / model, plus a config dialog: model, fallback, temperature slider, max tokens, response format, graph topology, max revisions, quality checks, system prompt textarea, tool toggles, and a built-in Test Runner (sample input → live trace: output, tool calls, retrieval hits, revisions, quality issues, ms, model used). - Tools (
AIToolsPanel.tsx) — tool registry table with category badges, mutates flag, schema viewer, edit description dialog. Read-only for the schema (it ships with the addon). - Prompts — original prompt editor logic preserved as
AIPromptsPanel; nothing was removed.
- Agents (
- Sidebar —
AdminLmsLayout.tsxupdatednav.aiPrompts→nav.aiAgentsso the menu item now reads "AI Agents & Tools". - i18n —
i18n/locales/{en,ar}.tsextended withaiAdmin,agents,toolsnamespaces (≈80 keys each), pluscommon.saving/common.disabled. Arabic strings preserve technical product names ("LangGraph", "ReAct") in Latin script.
22.4 Verification
Confirmed end-to-end during the §23 test run:
simpletopology (writing_grader) — POST/api/ai/agents/writing_grader/testreturned a structured score envelope in 3.3 s,model_used=gpt-4o, no fallback.reacttopology (lms_tutor) — same endpoint with a tutoring question executed two real tool calls (student.profile,resources.search), 13 s total, returned a CEFR-adapted reply and the tool trace.- Graph cache invalidation — editing
temperaturefrom 0.6 → 0.4 onlms_tutorand re-running confirmed the next call recompiled and used the new value. - Feature flag — flipping
encoach_ai.use_langgraph_runtimetoFalseand regenerating a course plan kept the API contract stable; flipping back restored the LangGraph trace.
22.5 What this unlocks
- Per-feature model swap — admin can move
course_plannertogpt-4o-minifor a cost test without touching code. - Tool curation — restricting
lms_tutorto read-only tools is a single checkbox; mutating tools (yellow badge) are deliberately separate. - Quality gates — flipping
quality_checks=quality.cefr_check,quality.content_gateonexam_generatorruns both gates before the response is accepted. - Future agents — adding a new agent is a
<record>inagents_defaults.xml(or a one-row INSERT). Adding a new tool is a@registerdecorator + a JSON schema; the UI picks it up automatically.
23. Full Demo Seed + 8-Role E2E Test (2026-04-25)
Once the LangGraph layer was in (§22), the remaining gap was that the demo dataset only exercised three product roles (admin, teacher, student). The product supports seven user_type values (student, teacher, admin, corporate, mastercorporate, agent, developer) plus a procedural eighth role (a teacher acting as approver). This pass fills every role with believable data and verifies every API surface end-to-end.
23.1 Scope
| Goal | Status |
|---|---|
Every user_type represented by a demo account |
DONE — see §3 |
| Active multi-stage approval workflow with one pending request | DONE |
| GE1-aligned 12-week B1 course plan (matches UTAS General English 1 Fall AY25-26 outline) | DONE |
| Six full Week-1 teaching materials (reading / writing / listening / speaking / grammar / vocabulary) | DONE |
Sample AI telemetry (encoach.ai.log) and encoach.ai.feedback for the three live agents |
DONE |
| Read-only API smoke for all 8 roles | 46/46 PASS |
| Mutation E2E for the full approval chain | 6/6 PASS |
| LangGraph live round-trip during the run | 2/2 topologies verified |
23.2 New scripts (workspace root, all idempotent)
| Script | Purpose |
|---|---|
seed_full_demo.py |
Adds 5 missing user types, activates a 2-stage exam-approval workflow with one pending request, creates a GE1 12-week B1 plan, populates Week 1 with 6 detailed materials, inserts sample ai.log + ai.feedback rows. Re-running is safe — every record is upserted by stable key (xml_id / login / unique pair). |
reset_demo_passwords.py |
Forces every demo user's password back to its canonical value via the Odoo ORM (res.users.write({'password': ...})). Use this any time test logins start failing — covers password drift caused by manual changes during interactive testing. |
e2e_full_scenario.py |
Read-only API smoke. Logs in as each of 8 roles and exercises the routes that role typically uses: profile, branding, courses, course plans, exams, attempts, AI agents, AI feedback, training, payments, reports. Prints a per-step PASS/FAIL line and a final summary. |
e2e_approval_chain.py |
Mutation E2E. Walks: approver logs in → lists pending requests → approves stage 1 → admin logs in → approves stage 2 (final) → linked encoach.exam.custom flips to status='published'. Verifies the underlying DB row at every step. |
23.3 Demo dataset snapshot (after running both seeders)
- Users — 12 total (see §3): 1 superadmin, 1 admin, 3 students, 3 teachers (one acting as approver), 1 corporate, 1 mastercorporate, 1 agent, 1 developer.
- Approval workflow —
Exam Approval Workflow(active=True, 2 stages: Coach Approval →approver@encoach.test, Admin Approval →admin@encoach.test). One pendingencoach.approval.requestlinked to an existingencoach.exam.customrow, sitting at stage 1. - Course plan —
GE1 — General English 1 (B1)linked to a newop.course(code=GE1,cefr_level=b1). 12 weekly rows pre-populated with theme + skill focus matching the UTAS outline. - Week 1 materials — six rows on
encoach.course.plan.materialcovering the exact outcomes the user pasted from the GE1 brief:- Reading text + 5 comprehension questions (~400 words at B1, scan + context-clue practice).
- Writing prompt (people / places / activities, ≥150 words, paragraph plan).
- Listening script + 6 questions (4-min dialogue, locally familiar topic).
- Speaking prompt (describe present / past / future activity, useful-language chunks).
- Grammar mini-lesson (Present Simple vs Present Continuous, rule + 3 examples + 5 practice items + answer key).
- Vocabulary list (10 entries × {pos, B1 definition, example sentence}).
- AI telemetry — sample
encoach.ai.logrows forwriting_grader,speaking_grader,lms_tutor(service='openai',action,model_used, token counts, input/output previews) andencoach.ai.feedbackrows (subject_type='other', mix ofrating='up'/'down').
23.4 Read-only smoke — e2e_full_scenario.py — 46/46 PASS
Endpoint coverage by role (every entry returned 2xx):
| Role | Login | Endpoints exercised | Result |
|---|---|---|---|
| superadmin | admin |
/api/login, /api/user, /api/ai/agents, /api/ai/tools, /api/courses, /api/exams, /api/reports/filters, /api/branding/1 |
8/8 |
| admin | admin@encoach.test |
as superadmin + /api/ai/feedback, /api/approvals/pending |
10/10 |
| teacher | khalid@encoach.test |
/api/login, /api/user, /api/ai/course-plan, /api/courses, /api/exams, /api/rubrics, /api/exam-structures |
7/7 |
| approver | approver@encoach.test |
/api/login, /api/user, /api/approvals/pending, /api/approvals/mine |
4/4 |
| student | sarah@encoach.test |
/api/login, /api/user, /api/courses, /api/exam-assignments, /api/training/vocabulary, /api/training/grammar |
6/6 |
| corporate | corporate@encoach.test |
/api/login, /api/user, /api/branding/1, /api/reports/stats-corporate, /api/payment-records |
5/5 |
| mastercorporate | master@encoach.test |
/api/login, /api/user, /api/reports/student-performance |
3/3 |
| agent | agent@encoach.test |
/api/login, /api/user, /api/codes, /api/packages |
2/2 |
| developer | dev@encoach.test |
/api/login, /api/user |
1/1 |
Live LangGraph hit during the same run: POST /api/ai/agents/writing_grader/test returned a fully scored envelope with model_used=gpt-4o, ms≈3300. The exact JSON is captured in docs/ENCOACH_FULL_DEMO_QA_REPORT.md.
23.5 Mutation E2E — e2e_approval_chain.py — 6/6 PASS
Steps walked, each verified against the database via psql:
- Approver login —
approver@encoach.test / approver123returns a JWT;/api/userconfirmsuser_type='teacher'. - Pending list —
/api/approvals/pendingreturns one request whosecurrent_stage_id.approver_id == approver. - Stage 1 approve —
POST /api/approvals/<id>/approveflipscurrent_stage_idto Admin Approval; DB showsstate='in_progress'(still active, advanced). - Admin login —
admin@encoach.test / admin123returns a JWT. - Final approve —
POST /api/approvals/<id>/approveflipsstate='approved'; the linkedencoach.exam.custom.statusflips to'published'via the post-approval hook. - DB verification —
select state from encoach_approval_request where id=<id>;returnsapproved;select status from encoach_exam_custom where id=<exam_id>;returnspublished. Both confirmed live withpsql.
23.6 Reproduction
# 1. Seed (idempotent — safe to re-run any time)
cd /Users/yamenahmad/projects2026/odoo/odoo19/odoo
../.conda-envs/odoo19/bin/python odoo-bin shell -c ../odoo.conf --no-http --stop-after-init < ../seed_full_demo.py
../.conda-envs/odoo19/bin/python odoo-bin shell -c ../odoo.conf --no-http --stop-after-init < ../reset_demo_passwords.py
# 2. Make sure Odoo + frontend are running (see §4), then:
cd /Users/yamenahmad/projects2026/odoo/odoo19
.conda-envs/odoo19/bin/python e2e_full_scenario.py
.conda-envs/odoo19/bin/python e2e_approval_chain.py
Both scripts are network-side only (they hit http://localhost:8069/api/*), so they can also be pointed at a staging VPS by exporting BASE_URL=https://staging.encoach.com first.
23.7 Files added
seed_full_demo.py
reset_demo_passwords.py
e2e_full_scenario.py
e2e_approval_chain.py
docs/ENCOACH_FULL_DEMO_QA_REPORT.md # full QA write-up: credentials, dataset snapshot,
# per-endpoint PASS/FAIL, mutation chain proof,
# LangGraph live-run output
23.8 Gotchas resolved during this pass
/api/medoes not exist on this build — the correct profile endpoint is/api/user. Updated tests accordingly.khalid@encoach.testwas failing login because his password had drifted during earlier interactive testing.reset_demo_passwords.pynow restores all canonical passwords idempotently.encoach.ai.logfield names differ from a naive guess — the model usesservice,action,model_used,prompt_tokens,completion_tokens,total_tokens,input_preview,output_preview.encoach.ai.feedbackusessubject_type,subject_id,rating in {'up','down'}(NOT'thumbs_up'). The seeder now matches both definitions.- The exam approval post-approval hook only fires when the final stage approves, so step 3 above advances the request without publishing the exam yet — that's correct behaviour, just not obvious from the API alone. The DB-side verification in step 6 is what makes it observable.
24. AI Course-Plans — RAG, Multi-modal Media, Assignments (2026-04-25)
§22 made LangGraph the runtime; §24 puts a real product on top of it. The user asked for an AI-driven course-plan experience modelled on the GE1 outline: detect deliverables up-front (e.g. "12 reading texts, 12 listening scripts, 24 audio narrations"), let admins drop reference files / URLs / pasted notes the AI must respect (RAG), generate multi-modal training material (images via DALL-E 3, voice via AWS Polly / ElevenLabs, slideshow video via local ffmpeg), then assign the finished plan to a class or to specific students. This section is the implementation log.
24.1 What this section delivers
| Phase | Capability | Where it lives |
|---|---|---|
| A | Reference sources (file/URL/text) → pgvector, scoped to plan | encoach.course.plan.source + services/source_indexer.py |
| B | Deliverables preview & live progress strip | services/deliverables.py + /api/ai/course-plan/<id>/deliverables |
| C | On-demand audio / image / video per material + bulk-per-week | encoach.course.plan.media + services/media_service.py |
| D | Assign a plan to an op.batch or specific res.users |
encoach.course.plan.assignment |
| E | Student-side list + drilldown (read-only) | /api/student/course-plans* + /student/course-plans page |
| i18n | Full EN + AR strings for every new control | frontend/src/i18n/locales/{en,ar}.ts |
24.2 New / extended backend artefacts
backend/custom_addons/encoach_ai_course/
├── models/
│ ├── course_plan_source.py ← NEW RAG ingestion row (file|url|text)
│ ├── course_plan_media.py ← NEW audio|image|video asset
│ └── course_plan_assignment.py ← NEW visibility unit (batch|students)
├── services/
│ ├── source_indexer.py ← NEW PDF/DOCX/HTML extraction → pgvector
│ ├── deliverables.py ← NEW per-week status + percent_ready
│ └── media_service.py ← NEW Polly/ElevenLabs/DALL-E/ffmpeg
└── controllers/course_plan.py ← +18 endpoints (sources, media, assignments, student-side)
Existing models updated:
encoach.course.plangainssource_ids,media_ids,assignment_ids,to_api_dict(include_media=...).encoach.ai.tool.categoryselection extends with('media', 'Media generation')so the new tools register cleanly.encoach.embeddingqueries gain aplan_idfilter —resources.searchis now plan-scoped when passedplan_id, so the agent sees only its own corpus.
New tools (registered in encoach_ai/data/agents_defaults.xml):
| Tool key | Provider | Notes |
|---|---|---|
media.synthesize_audio |
AWS Polly (default) / ElevenLabs | Used for listening_script + speaking_prompt |
media.generate_image |
OpenAI DALL-E 3 | Capped per plan via encoach_ai_course.image_budget_per_plan (default 60) |
media.compose_video |
local ffmpeg |
Auto-creates audio + image first if missing |
New agent: course_media_director — orchestrates per-week media batches; reuses course_week_materials's tools plus the three media tools above. The base course_week_materials agent now also has media.synthesize_audio + media.generate_image in its tool set so a single LangGraph turn can emit the text material and its narration/illustration.
24.3 New API endpoints (all JWT-protected unless noted)
# Sources (Phase A)
POST /api/ai/course-plan/<id>/sources create URL or text source
POST /api/ai/course-plan/<id>/sources/upload multipart file upload
GET /api/ai/course-plan/<id>/sources list sources for plan
POST /api/ai/course-plan/sources/<sid>/reindex re-extract + re-embed
DELETE /api/ai/course-plan/sources/<sid> cascade-deletes embeddings
# Deliverables (Phase B)
GET /api/ai/course-plan/<id>/deliverables {summary, weeks: [...]}
# Media (Phase C)
GET /api/ai/course-plan/material/<mid>/media list media for material
POST /api/ai/course-plan/material/<mid>/media/audio generate audio
POST /api/ai/course-plan/material/<mid>/media/image generate image
POST /api/ai/course-plan/material/<mid>/media/video compose video
DELETE /api/ai/course-plan/media/<media_id> delete media + attachment
POST /api/ai/course-plan/<id>/week/<n>/media bulk-generate week (audio+image)
# Assignments (Phase D)
GET /api/ai/course-plan/<id>/assignments
POST /api/ai/course-plan/<id>/assignments
DELETE /api/ai/course-plan/assignments/<aid>
# Student-side (Phase E)
GET /api/student/course-plans list assigned plans
GET /api/student/course-plans/<id> read-only drilldown w/ media URLs
24.4 New / updated frontend surfaces
- Wizard (
pages/admin/wizards/CoursePlanWizard.tsx): now 6 steps — Brief → Resources (source builder) → Deliverables preview → Multimedia toggles → Review → Generate. Sources are queued client-side and uploaded post-creation against the new plan id. - Detail page (
pages/admin/AdminCoursePlanDetail.tsx): now showsDeliverablesStrip,SourcesCard,AssignmentsCard(withAssignDialog), and per-materialMediaDrawer(audio/image/video preview, generate, download, delete). Plus a "Generate week media" bulk action per week. - Student (
pages/student/StudentCoursePlans.tsx,StudentCoursePlanDetail.tsx): list + read-only drilldown,<audio>/<img>/<video>tiles fed from/web/content/<attachment_id>. - Sidebar: new "My Course Plans" entry under student nav (
StudentLayout.tsx). - i18n: ~140 new keys in EN + AR across
coursePlan.wizard.*,coursePlan.sources.*,coursePlan.deliverables.*,coursePlan.media.*,coursePlan.assignments.*,coursePlan.student.*,nav.myCoursePlans.
24.5 Smoke test (run via odoo-bin shell)
smoke_course_plan.py exercises every phase end-to-end:
| Phase | Result |
|---|---|
| A — index inline-text source | PASS · 1 chunk · 295 chars (uses local MiniLM embeddings) |
B — compute_deliverables |
PASS · 12 weeks · 5 ready / 1 generated / 0 planned |
| C — DALL-E 3 image | PASS · 753 KB PNG generated for a reading_text material |
| C — Polly audio | Gracefully marked failed with "AWS credentials not configured — set in AI Settings" (expected on dev box) |
| D — assignment | PASS · created mode='students' assignment for Sarah |
| E — visibility | PASS · expand_user_ids() returns Sarah · plan in student listing |
Run it any time:
.conda-envs/odoo19/bin/python odoo/odoo-bin shell \
-c odoo.conf -d encoach_v2 --no-http \
< smoke_course_plan.py
24.6 Operational notes
- External providers are optional. AWS Polly / ElevenLabs / OpenAI image keys live in AI Settings (
encoach.ai.settings); when missing the corresponding media row is created withstatus='failed'and a clear error — the rest of the flow (planning, RAG, deliverables, assignment) still works. ffmpegmust be onPATHfor video composition. If it's missing, the video media row isfailedwith a one-line install hint; nothing else breaks.- Image budget per plan is the system parameter
encoach_ai_course.image_budget_per_plan(default 60). Bump it via Settings → Technical → System Parameters. - Plan-scoped retrieval. The
resources.searchtool now accepts aplan_id; the LLM is auto-given the plan id in the system prompt ofcourse_plannerandcourse_week_materials, so RAG only ever sees that plan's own sources. - Cascade behaviour. Deleting a source unlinks its
encoach.embeddingrows; deleting a plan unlinks sources, media, assignments, and the underlyingir.attachments.
24.7 Files added / changed in this pass
backend/custom_addons/encoach_ai_course/models/course_plan_source.py + new
backend/custom_addons/encoach_ai_course/models/course_plan_media.py + new
backend/custom_addons/encoach_ai_course/models/course_plan_assignment.py + new
backend/custom_addons/encoach_ai_course/services/source_indexer.py + new
backend/custom_addons/encoach_ai_course/services/deliverables.py + new
backend/custom_addons/encoach_ai_course/services/media_service.py + new
backend/custom_addons/encoach_ai_course/controllers/course_plan.py ~ +480 LOC
backend/custom_addons/encoach_ai_course/security/ir.model.access.csv ~ access rights
backend/custom_addons/encoach_ai/models/ai_agent.py ~ TOOL_CATEGORIES += media
backend/custom_addons/encoach_ai/data/agents_defaults.xml ~ + 3 tools, + 1 agent
backend/custom_addons/encoach_vector/models/embedding.py ~ plan_id filter
frontend/src/types/coursePlan.ts ~ +4 interfaces
frontend/src/services/coursePlan.service.ts ~ +18 methods
frontend/src/pages/admin/wizards/CoursePlanWizard.tsx ~ +SourcesStep, MediaStep
frontend/src/pages/admin/AdminCoursePlanDetail.tsx ~ rewrite (Deliverables, Sources, Assignments, MediaDrawer)
frontend/src/pages/student/StudentCoursePlans.tsx + new
frontend/src/pages/student/StudentCoursePlanDetail.tsx + new
frontend/src/App.tsx ~ student routes
frontend/src/components/StudentLayout.tsx ~ My Course Plans link
frontend/src/i18n/locales/en.ts ~ +140 keys
frontend/src/i18n/locales/ar.ts ~ +140 keys
smoke_course_plan.py + new (E2E smoke test)
24.8 Gotchas resolved during this pass
encoach.ai.tool.categoryis a strictSelection; adding the three media tools required adding('media', 'Media generation')toTOOL_CATEGORIESinai_agent.py. Without that the data file refused to load and the wholeencoach_aimodule rolled back its upgrade — silently leaving the new course-plan tables uncreated until we restarted with-u encoach_vector,encoach_ai,encoach_ai_course.- Odoo XML data loaders refuse two
<record id="..."/>with the same id in the same file (no implicit merge). Thecourse_week_materialsagent had to be edited in place with the new media tools instead of being patched as a second record at the bottom. pypdfextracts most encrypted-but-public PDFs; if bothpypdfandPyPDF2are missing the indexer marks the sourcefailedwith a clear pip hint rather than crashing.- The student-side endpoint uses
expand_user_ids()so amode='batch'assignment auto-includes any future student rolled into that batch — no need to re-assign per intake.