Teacher Analytics Dashboard (v1)
Background
sysmex already stores rich learning and conversation data for a published course (Shifu). This document proposes a teacher-facing dashboard (initial integration) to help course creators/collaborators understand:
- Learner progress
- Course completion
- Follow-up questions (ASK) metrics
- Learner personalization (profiles/variables)
- Follow-up details (Q/A logs)
Requirements:
- Provide detailed views (tables + drill-down per learner)
- Provide chart analysis (ECharts)
- Frontend chart library:
echarts+echarts-for-react - Prefer additive changes with minimal disturbance to existing business logic
- Make chart components wrappable/composable for future reuse
What We Have Today (Relevant Data Sources)
Course structure (published)
shifu_log_published_structs(flaskr.service.shifu.models.LogPublishedStruct)- JSON serialized
HistoryItemtree (type:shifu,outline,block)
- JSON serialized
shifu_published_outline_items(PublishedOutlineItem)outline_item_bid,title,type(trial/normal/guest),hidden
This is the source of truth for what learners can study in production mode.
Learner progress
learn_progress_records(flaskr.service.learn.models.LearnProgressRecord)- Key fields:
shifu_bid,outline_item_bid,user_bidstatus(LEARN_STATUS_*)block_position(coarse pointer inside an outline’s block list)updated_at(used as “last activity” proxy)
- Note: there can be multiple records per
(user_bid, outline_item_bid); code paths often pick the latest byid.
- Key fields:
Follow-up Q/A logs (追问)
learn_generated_blocks(LearnGeneratedBlock)- Follow-up handler:
flaskr.service.learn.handle_input_ask.handle_input_ask - For follow-ups:
- Student question:
type = BLOCK_TYPE_MDASK_VALUE,role = ROLE_STUDENT - Teacher answer:
type = BLOCK_TYPE_MDANSWER_VALUE,role = ROLE_TEACHER
- Student question:
- Has timestamps:
created_at, plusoutline_item_bid,progress_record_bid,position.
- Follow-up handler:
Personalization data
- Profile item definitions:
flaskr.service.profile.profile_manage.get_profile_item_definition_list(parent_id=shifu_bid) - User variable values:
var_variable_values(flaskr.service.profile.models.VariableValue)- Global/system scope values:
shifu_bid == ""(core profile labels) - Course scope values:
shifu_bid == <course_id>(custom variables collected during learning) - Read helper used in learning runtime:
flaskr.service.profile.funcs.get_user_profiles
- Global/system scope values:
Enrollment candidates (optional enhancement)
order_orders(flaskr.service.order.models.Order)- Can be used to include “purchased but never started” learners.
- V1 can start with “learners with progress records”; optionally union orders later.
Metrics Definitions (V1)
Outline set (what counts toward progress)
Default for V1:
- Use published outline items from
LogPublishedStruct+PublishedOutlineItem. - Exclude
hidden == 1. - Count only
type == UNIT_TYPE_VALUE_NORMALas “required lessons”.
Optional flags for future:
include_trial=true: include trial outlinesinclude_guest=true: include guest outlines
Learner set (who is included)
Default for V1:
- Learners are users who have at least one
LearnProgressRecordfor thisshifu_bid(latest non-reset record).
Optional later:
- Union in paid orders (
Order.status == ORDER_STATUS_SUCCESS) to include not-started learners.
Per-learner summary fields
required_outline_totalcompleted_outline_countin_progress_outline_countprogress_percent = completed / total(0..1)last_active_at = max(updated_at)across latest progress recordsfollow_up_ask_count = count(MDASK)(time-range aware for charting; total for per-learner)
Course-level overview
learner_countcompletion_count(learners withcompleted == total)completion_rateprogress_distribution(bucketed)follow_up_trend(daily count within date range)top_outlines_by_follow_upstop_learners_by_follow_ups
Backend Design
New service module
Add a new additive service module:
src/api/flaskr/service/dashboard/dtos.py(Pydantic DTOs with__json__)funcs.py(query/aggregation helpers)routes.py(HTTP routes,@inject)
Permission model
Teacher dashboard must be restricted:
- Require login (existing
before_requestsetsrequest.user) - Require Shifu permission:
shifu_permission_verification(app, request.user.user_id, shifu_bid, "view")
Proposed endpoints (V1)
All endpoints under /api/dashboard, additive to avoid disturbing existing routes.
-
GET /api/dashboard/shifus/{shifu_bid}/outlines- Returns published outline list used for progress calculation and filtering.
-
GET /api/dashboard/shifus/{shifu_bid}/overview- Query params:
start_date(YYYY-MM-DD, optional)end_date(YYYY-MM-DD, optional)include_trial/include_guest(optional)
- Returns KPI + chart-ready series (daily arrays, top-N arrays, buckets).
- Query params:
-
GET /api/dashboard/shifus/{shifu_bid}/learners- Query params:
page_index(default 1)page_size(default 20, max 100)keyword(optional; matches user_bid / mobile / nickname)sort(optional; e.g.last_active_at_desc,progress_desc,followups_desc)
- Returns paginated learner summaries.
- Query params:
-
GET /api/dashboard/shifus/{shifu_bid}/learners/{user_bid}- Returns:
- outline progress statuses (per outline)
- learner core info
- learner course-scoped variables (from
get_user_profiles) - follow-up aggregates (count by outline, recent items)
- Returns:
-
GET /api/dashboard/shifus/{shifu_bid}/learners/{user_bid}/followups- Query params:
outline_item_bid(optional filter)start_time,end_time(optional, ISO orYYYY-MM-DD)page_index,page_size
- Returns follow-up items (question/answer, timestamps, outline info).
- Query params:
Data access patterns (important implementation notes)
Hard constraint: no database JOIN queries.
- Do not use SQL JOIN / SQLAlchemy
.join()/ relationship eager-loading to combine tables. - For parent/child lookups, always:
- Query the parent table first to get the parent keys (
*_bid,id,parent_bid, etc.). - Query the child table with
IN (...)using those keys. - Combine the result sets in Python with dict maps.
- Query the parent table first to get the parent keys (
- If an
IN (...)list can grow large, chunk it (e.g. 500-1000 ids per query) and merge the chunks in memory.
Examples:
- Published outlines:
- Load
LogPublishedStruct(parent) to obtain the outlineid/outline_item_bidlist. - Load
PublishedOutlineItem(child) withPublishedOutlineItem.id.in_(...). - Merge by
outline_item_bidin Python.
- Load
- Learner list:
- Load latest
LearnProgressRecordrows first (parent) and collectuser_bidlist. - Load
UserEntity(child) withUserEntity.user_bid.in_(...). - Load
AuthCredential(child) withAuthCredential.user_bid.in_(...). - Merge user + credential + progress in Python.
- Load latest
Other important patterns:
- Always use latest progress record per
(user_bid, outline_item_bid):- Build a
max(id)subquery grouped by(user_bid, outline_item_bid)withstatus != LEARN_STATUS_RESETanddeleted == 0, then load full rows viaLearnProgressRecord.id.in_(subquery).
- Build a
- Avoid N+1 by batching with
IN (...)queries:- Batch-load users/credentials for learner lists.
- Batch-load ask counts via grouped queries on
learn_generated_blocks(no joins).
- Time range for trends:
DATE(created_at)group-by for follow-up trend.
Swagger schemas
Follow existing convention:
@register_schema_to_swagger- DTOs are
pydantic.BaseModelwith explicit__json__.
Frontend Design (Cook Web)
Route
Add a new admin page:
src/cook-web/src/app/admin/dashboard/page.tsx
Minimal existing changes:
- Add a new menu item in
src/cook-web/src/app/admin/layout.tsxpointing to/admin/dashboard.
API client integration
Add new endpoints to:
src/cook-web/src/api/api.ts
Then use the generated functions via import api from '@/api'.
Chart component architecture (ECharts)
Add reusable primitives:
src/cook-web/src/components/charts/EChart.tsx- Client-only wrapper around
echarts-for-reactvianext/dynamic - Props:
option,style,loading,onEvents,opts,notMerge,lazyUpdate
- Client-only wrapper around
src/cook-web/src/components/charts/ChartCard.tsx- Standardized card chrome: title, subtitle, actions slot, chart area
Option builders (future-friendly):
src/cook-web/src/lib/charts/options.tsbuildLineOption(...)buildBarOption(...)- Common theme tokens aligned with Tailwind variables
UI layout (V1)
Dashboard page sections:
- Header controls
- Course selector (reuses existing shifu list API)
- Date range selector (reuse
Calendar/Popoverpatterns from admin orders)
- KPI cards
- Learners, completion rate, follow-up totals, active learners (optional)
- Charts grid
- Progress distribution (bar)
- Follow-up trend (line)
- Top outlines by follow-ups (bar)
- Learner table + drill-down
- Table: learner info + progress + follow-ups + last active
- Row click opens a
Sheetwith tabs:- Progress (outline list statuses)
- Follow-ups (Q/A list with filters)
- Personalization (variables table)
i18n
Add a new namespace file:
src/i18n/en-US/modules/dashboard.jsonsrc/i18n/zh-CN/modules/dashboard.json
Keys example:
module.dashboard.titlemodule.dashboard.kpi.learnersmodule.dashboard.chart.followUpsTrendmodule.dashboard.table.progress
Testing & Validation
Backend:
- Add API tests under
src/api/tests/service/dashboard/ - Focus on:
- permission enforcement
- outline set correctness (hidden excluded)
- “latest record” selection correctness
- pagination stability
Frontend:
- Basic render tests for chart wrappers (Jest)
- Manual QA checklist:
- load dashboard
- switch course
- switch date range
- open learner detail and paginate follow-ups
Risks / Open Questions
- Learner set definition: should it include “purchased but not started” by default?
- Progress precision:
block_positionenables intra-outline progress, but total blocks length requires parsing MarkdownFlow; V1 uses outline-level completion. - PII exposure: showing mobile/email in teacher dashboard might require masking or role-based gating.
- Performance: large courses may need caching/materialized aggregates and better DB indexes (post-V1).