---
title: Teacher Analytics Dashboard (v1)
status: implemented
owner_surface: shared
last_reviewed: 2026-04-17
canonical: true
---

# Teacher Analytics Dashboard (v1)

## Background

AI-Shifu 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:

1. Learner progress
2. Course completion
3. Follow-up questions (ASK) metrics
4. Learner personalization (profiles/variables)
5. 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 `HistoryItem` tree (type: `shifu`, `outline`, `block`)
- `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_bid`
    - `status` (`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 by `id`.

### 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`
  - Has timestamps: `created_at`, plus `outline_item_bid`, `progress_record_bid`, `position`.

### 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`

### 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_NORMAL` as “required lessons”.

Optional flags for future:

- `include_trial=true`: include trial outlines
- `include_guest=true`: include guest outlines

### Learner set (who is included)

Default for V1:

- Learners are users who have **at least one** `LearnProgressRecord` for this `shifu_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_total`
- `completed_outline_count`
- `in_progress_outline_count`
- `progress_percent = completed / total` (0..1)
- `last_active_at = max(updated_at)` across latest progress records
- `follow_up_ask_count = count(MDASK)` (time-range aware for charting; total for per-learner)

### Course-level overview

- `learner_count`
- `completion_count` (learners with `completed == total`)
- `completion_rate`
- `progress_distribution` (bucketed)
- `follow_up_trend` (daily count within date range)
- `top_outlines_by_follow_ups`
- `top_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_request` sets `request.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.

1. `GET /api/dashboard/shifus/{shifu_bid}/outlines`
   - Returns published outline list used for progress calculation and filtering.

2. `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).

3. `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.

4. `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)

5. `GET /api/dashboard/shifus/{shifu_bid}/learners/{user_bid}/followups`
   - Query params:
     - `outline_item_bid` (optional filter)
     - `start_time`, `end_time` (optional, ISO or `YYYY-MM-DD`)
     - `page_index`, `page_size`
   - Returns follow-up items (question/answer, timestamps, outline info).

### 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:
  1. Query the parent table first to get the parent keys (`*_bid`, `id`, `parent_bid`, etc.).
  2. Query the child table with `IN (...)` using those keys.
  3. Combine the result sets in Python with dict maps.
- 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 outline `id` / `outline_item_bid` list.
  - Load `PublishedOutlineItem` (child) with `PublishedOutlineItem.id.in_(...)`.
  - Merge by `outline_item_bid` in Python.
- Learner list:
  - Load latest `LearnProgressRecord` rows first (parent) and collect `user_bid` list.
  - Load `UserEntity` (child) with `UserEntity.user_bid.in_(...)`.
  - Load `AuthCredential` (child) with `AuthCredential.user_bid.in_(...)`.
  - Merge user + credential + progress in Python.

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)` with `status != LEARN_STATUS_RESET` and `deleted == 0`, then load full rows via `LearnProgressRecord.id.in_(subquery)`.
- 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.BaseModel` with 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.tsx` pointing 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-react` via `next/dynamic`
  - Props: `option`, `style`, `loading`, `onEvents`, `opts`, `notMerge`, `lazyUpdate`
- `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.ts`
  - `buildLineOption(...)`
  - `buildBarOption(...)`
  - Common theme tokens aligned with Tailwind variables

### UI layout (V1)

Dashboard page sections:

1. Header controls
   - Course selector (reuses existing shifu list API)
   - Date range selector (reuse `Calendar` / `Popover` patterns from admin orders)
2. KPI cards
   - Learners, completion rate, follow-up totals, active learners (optional)
3. Charts grid
   - Progress distribution (bar)
   - Follow-up trend (line)
   - Top outlines by follow-ups (bar)
4. Learner table + drill-down
   - Table: learner info + progress + follow-ups + last active
   - Row click opens a `Sheet` with 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.json`
- `src/i18n/zh-CN/modules/dashboard.json`

Keys example:

- `module.dashboard.title`
- `module.dashboard.kpi.learners`
- `module.dashboard.chart.followUpsTrend`
- `module.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

1. **Learner set definition**: should it include “purchased but not started” by default?
2. **Progress precision**: `block_position` enables intra-outline progress, but total blocks length requires parsing MarkdownFlow; V1 uses outline-level completion.
3. **PII exposure**: showing mobile/email in teacher dashboard might require masking or role-based gating.
4. **Performance**: large courses may need caching/materialized aggregates and better DB indexes (post-V1).
