Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,64 @@ scoring — the `pipeline/` rubric is authoritative. The old Zoom ±5 ingest
- `POST /api/admin/chat-sp-reviews/:id/accept` — award SP
- `POST /api/admin/chat-sp-reviews/:id/reject` — reject

## Engagement Classification (Chunks 1–7, 2026-07-15)

Classifies students into 4 engagement bands based on a rolling window of sessions.

### Files
- `server/engagement/config.js` — rolling window size (N=3), band thresholds, 4 band labels
- `server/engagement/fetchData.js` — fetches attendance + SP transactions, splits into current/previous windows
- `server/engagement/classifyBand.js` — pure function: `classifyBand(current, previous)` → `{ band, reason, stats }`
- `server/routes/engagement.js` — Express router for the single-student endpoint

### Bands
| Band | Criteria | Description |
|------|----------|-------------|
| **Excellent** | avg attendance ≥90%, avg SP ≥8/session | High attendance, strong SP gain |
| **Active** | avg attendance ≥75%, avg SP ≥3/session | Consistent attendance, moderate SP |
| **Slowing Down** | avg attendance <75% OR declining trend | Dropping off, risk of falling behind |
| **Recovery** | prior window was Slowing Down, now improving | Trend reversal detected |

### Endpoints
- `GET /api/engagement/:email` — Single student engagement band + window summary
- Response: `{ email, name, totalSp, band, reason, stats, windows: { current, previous } }`
- `GET /api/admin/engagement/report` — All active students grouped by band (admin auth required)
- Optional: `?band=Excellent|Active|Slowing Down|Recovery` to filter
- Response: `{ summary: { Excellent: { count }, ... }, total, groups }`
- Auth headers: `x-admin-email: dled@iitrpr.ac.in`, `x-admin-token: vled-local-admin`

## Journey Tracker (Chunks 1–9, 2026-07-16)

Tracks student progress across three time windows with admin-configurable targets.

### Files
- `server/models/JourneyTarget.js` — Mongoose model for DB-stored target configs
- `server/journey/targets.js` — defaults + get/upsert helpers (fallback to defaults if no DB entry)
- `server/journey/dateRange.js` — resolves `weekly|monthly|tenure` to `{ start, end, label }`
- `server/journey/computeJourney.js` — fetches attendance+polls within range, computes overall %, determines checkpoints
- `server/routes/journey.js` — Express router for the student-facing endpoint
- `client/src/components/journey/JourneyTracker.jsx` — student-facing route with checkpoint dots + metric bars
- `client/src/components/journey/AdminJourneyTargets.jsx` — admin form to edit targets

### Default Targets
| Window | Checkpoints | Att Target | Poll Target | Weight |
|--------|-------------|-----------|-------------|--------|
| Weekly | 5 | 80% | 75% | 50/50 |
| Monthly | 4 | 85% | 80% | 50/50 |
| Tenure | 8 | 75% | 70% | 50/50 |

### Progress Calculation
- `overallPct = attendancePct * (attendanceWeight/100) + pollPct * (pollWeight/100)`
- Checkpoints reached = `floor(elapsedTimeRatio * checkpointCount)`

### Endpoints
- `GET /api/journey/:email?window=weekly|monthly|tenure` — Student journey progress
- Response: `{ window, range, target, progress, checkpoints, sessions }`
- `GET /api/admin/journey/targets` — All targets (DB override or default)
- `PUT /api/admin/journey/targets/:window` — Save custom target (admin auth required)
- Auth headers: `x-admin-email: dled@iitrpr.ac.in`, `x-admin-token: vled-local-admin`
- Body: `{ label, checkpointCount, attendanceTargetPct, pollTargetPct, attendanceWeight, pollWeight }`

## Auth — `chatengine_token` cookie passthrough (LIVE since 2026-06-29)
Spurti lives at `samagama.in/spurti` (same domain as Samagama), so the browser
already holds the student's **`chatengine_token`** cookie. There is **no login
Expand Down
1 change: 1 addition & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Summership SP Record</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
Expand Down
57 changes: 16 additions & 41 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
},
"dependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^5.4.11",
"analysis-summership": "file:..",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {}
"react-dom": "^18.3.1",
"vite": "^5.4.11"
}
}
115 changes: 115 additions & 0 deletions client/src/components/engagement/AdminBandGrid.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { useEffect, useState } from 'react';
import BlockIcon from './BlockIcon';

const BAND_ORDER = ['Excellent', 'Active', 'Recovery', 'Slowing Down'];

export default function AdminBandGrid({ auth }) {
const [groups, setGroups] = useState(null);
const [summary, setSummary] = useState(null);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState('');
const [loading, setLoading] = useState(true);
const [hovered, setHovered] = useState(null);

const headers = auth ? {
'X-Admin-Email': auth.email,
'X-Admin-Token': auth.token
} : {};

useEffect(() => {
if (!auth) return;
setLoading(true);
const base = window.location.pathname.startsWith('/spurti') ? '/spurti' : '';
const url = filter ? `${base}/api/admin/engagement/report?band=${encodeURIComponent(filter)}` : `${base}/api/admin/engagement/report`;
fetch(url, { headers })
.then(r => r.ok ? r.json() : null)
.then(d => {
if (!d) return;
if (filter) {
setGroups({ [filter]: d.students || [] });
setSummary({ [filter]: { count: d.count } });
setTotal(d.count);
} else {
setGroups(d.groups || {});
setSummary(d.summary || {});
setTotal(d.total || 0);
}
})
.catch(() => {})
.finally(() => setLoading(false));
}, [auth, filter]);

if (!auth) {
return <section className="panel"><p className="muted">Admin login required to view engagement report.</p></section>;
}

if (loading) {
return (
<section className="panel">
<div className="mc-loading">Loading report...</div>
</section>
);
}

const filteredGroups = filter ? { [filter]: groups?.[filter] || [] } : groups || {};

return (
<section className="panel">
<div className="panel-head">
<h2>Engagement Report</h2>
<span style={{ color: 'var(--muted)', fontSize: 13 }}>{total} students</span>
</div>

<div className="mc-filter-bar">
<button className={`mc-filter-btn ${!filter ? 'active' : ''}`} onClick={() => setFilter('')}>All</button>
{BAND_ORDER.map(band => (
<button
key={band}
className={`mc-filter-btn ${filter === band ? 'active' : ''}`}
onClick={() => setFilter(filter === band ? '' : band)}
>
{band}
</button>
))}
</div>

<div className="mc-grid-wrap">
{BAND_ORDER.map(band => {
const students = filteredGroups[band];
if (!students || students.length === 0) return null;
return (
<div key={band} className="mc-band-row">
<h3>
<BlockIcon band={band} size="sm" />
{band}
<span>{students.length} student{students.length !== 1 ? 's' : ''}</span>
</h3>
<div className="mc-grid">
{students.map(s => (
<div
key={s.email}
className="mc-grid-item"
onMouseEnter={() => setHovered(s.email)}
onMouseLeave={() => setHovered(null)}
>
<BlockIcon band={s.band} size="sm" />
{hovered === s.email && (
<div className="mc-tooltip" style={{ bottom: 'calc(100% + 4px)' }}>
<strong>{s.name}</strong><br />
{s.reason}
</div>
)}
</div>
))}
</div>
</div>
);
})}
</div>

{Object.keys(filteredGroups).length === 0 && (
<p className="muted" style={{ textAlign: 'center', padding: 20 }}>No students found in this band.</p>
)}
</section>
);
}
Loading