Skip to content

Commit 6aeb82d

Browse files
authored
fix: NQL rate formatting and course_enrollments routing (#90) (#91)
* feat: add cohort and enrollment intensity filters to student roster (#81) (#82) * docs: add demo script with 6-minute talk track and screenshot guide * docs: move DEMO.md into docs/ directory * docs: move demo script and screenshots into docs/demo/ subdirectory * chore: untrack large presentation files, add *.pptx and docs PDFs to .gitignore * feat: student roster page with drill-down, filtering, sorting, and CSV export (#65) * feat: student roster with info popovers; fix gateway models using 'Y' not 'C' for completion target * fix: credential model — add sought-credential fallback and class_weight=balanced; add sorting for enrollment and credential type columns * fix: update credential type popover to reflect sought-credential fallback logic * feat: dashboard filtering by cohort, enrollment type, and credential type (#66) (#72) - Add filter bar above KPI tiles with Cohort, Enrollment Type, and Credential Type dropdowns (shadcn Select) and a Clear button with filtered-student count - All 4 dashboard API routes now accept cohort, enrollmentType, credentialType query params and apply parameterized WHERE clauses - Risk alerts and retention-risk routes use a CTE so percentage denominators are relative to the filtered set (not the full table) - Readiness route conditionally JOINs student_level_with_predictions when enrollment or credential filters are active; existing institution/cohort/level params unchanged - All fetch calls on the page are re-triggered when filter state changes * feat: audit log export endpoint with CSV download button (#67) (#73) - Add GET /api/query-history/export that reads logs/query-history.jsonl and streams a CSV with headers: timestamp, institution, prompt, vizType, rowCount - Accepts optional ?from=ISO_DATE&to=ISO_DATE query params for date-range filtering; returns 404 with clear message if the log file does not exist yet - Sets Content-Disposition: attachment; filename="query-audit-log.csv" - Add "Export" button with download icon in the QueryHistoryPanel header that triggers a direct browser download via <a href download> * feat: student detail view with personalized recommendations (#77) (#79) - Add GET /api/students/[guid] joining student_level_with_predictions + llm_recommendations; returns 404 for unknown GUIDs - Add /students/[guid] page with: - Student header: GUID, cohort, enrollment, credential, at-risk + readiness badges - FERPA disclaimer (de-identified GUID only, no PII stored) - Six prediction score cards (retention, readiness, gateway math/English, GPA risk, time-to-credential) color-coded green/yellow/red - AI Readiness Assessment card: rationale, risk factors (orange dot list), and recommended actions (checkbox-style checklist) - Graceful fallback when no assessment has been generated yet - Back button uses router.back() to preserve roster filter state - Student roster rows are now fully clickable (onClick → router.push) with the GUID cell retaining its Link for ctrl/cmd+click support * feat: Supabase Auth + role-based access control (FR6, #75) (#80) * feat: Supabase Auth + role-based access control (FR6, #75) Auth layer - Install @supabase/supabase-js + @supabase/ssr - lib/supabase/client.ts — browser client (createBrowserClient) - lib/supabase/server.ts — server client (createServerClient + cookies) - lib/supabase/middleware-client.ts — session refresh helper for middleware Roles - lib/roles.ts — Role type, ROUTE_PERMISSIONS map, canAccess() helper, ROLE_LABELS and ROLE_COLORS per role - Five roles: admin | advisor | ir | faculty | leadership /students/** → admin, advisor, ir /query → admin, advisor, ir, faculty /api/students/** → admin, advisor, ir /api/query-history/export → admin, ir / and /methodology → all roles (public within auth) Middleware - middleware.ts — unauthenticated → redirect /login; role resolved from user_roles table; canAccess() enforced; role + user-id + email forwarded as request headers (x-user-role, x-user-id, x-user-email) for API routes Login page - app/login/page.tsx — email/password form using createBrowserClient - app/auth/callback/route.ts — PKCE code exchange handler Navigation - components/nav-header.tsx — sticky top bar: role badge, email, sign-out - app/layout.tsx — server component reads session + role, renders NavHeader when authenticated API guards - /api/students: 403 for faculty + leadership - /api/students/[guid]: 403 for faculty + leadership - /api/query-history/export: 403 for non-admin/ir Database & seed - migrations/001_user_roles.sql — user_roles table + RLS policy - scripts/seed-demo-users.ts — creates 5 demo users via service role key (admin/advisor/ir/faculty/leadership @bscc.edu, pw: BishopState2025!) * fix: seed script accepts NEXT_PUBLIC_ env var names; install tsx dev dep * feat: add cohort and enrollment intensity filters to student roster (#81) * fix: use correct DB enrollment intensity values (Full-Time/Part-Time with hyphens) * chore: add GitHub Actions CI/CD workflows (#83) (#84) * fix: drop npm lockfile cache since package-lock.json is gitignored npm ci requires a lockfile; switch to npm install in ci-dashboard and security-audit workflows to avoid cache resolution failures. * feat: course sequencing insights and DFWI analysis (#85) (#87) * feat: add course_enrollments migration and data ingestion script (#85) * fix: transaction safety and validation in course enrollment ingestion (#85) * feat: add course DFWI, gateway funnel, and sequence API routes (#85) * fix: sequence join granularity, gateway funnel clarity, DFWI result cap (#85) * feat: add /courses page with DFWI table, gateway funnel, and co-enrollment pairs (#85) * fix: percentage display and component cleanup in /courses page (#85) * fix: gateway type label values (M/E) and add RBAC to gateway-funnel route (#85) * feat: sortable column headers, info popovers for DFWI/pass rate, pairings table sort (#85) * feat: tabbed courses page with AI-powered co-enrollment explainability - Redesign /courses page with 3 tabs: DFWI Rates, Gateway Funnel, Co-enrollment Insights - Add POST /api/courses/explain-pairing route: queries per-pair stats (individual DFWI/pass rates, breakdown by delivery method and instructor type) then calls gpt-4o-mini to generate an advisor-friendly narrative - Co-enrollment Insights tab shows sortable pairings table with per-row Explain button that fetches and renders stats chips + LLM analysis inline - Tab state is client-side (no Radix Tabs dependency needed) * ci: trigger re-run after workflow fix * fix: update ESLint for Next.js 16 (next lint removed) - Replace next lint with direct eslint . in package.json lint script - Rewrite eslint.config.mjs to use eslint-config-next flat config exports directly instead of deprecated FlatCompat bridge - Add eslint and eslint-config-next as devDependencies - Suppress pre-existing rule violations (no-explicit-any, no-unescaped-entities, set-state-in-effect) to avoid CI failures on legacy code * feat: NQL interface redesign — nav link, sidebar history, LLM Summarize (#88) (#89) * docs: NQL interface redesign design doc (#88) * docs: NQL redesign implementation plan (#88) * feat: add Query link to global nav header (#88) * feat: add POST /api/query-summary LLM result narration (#88) * fix: harden query-summary route input validation (#88) * refactor: adapt QueryHistoryPanel for sidebar layout (#88) * fix: restore institution label, fix text size, restore truncation threshold (#88) * feat: sidebar layout + LLM Summarize button on query page (#88) * fix: NQL rate formatting and course_enrollments routing (#90) - Add isRateColumn helper and formatCellValue to render 0–1 probabilities as percentages in table cells, chart axes, tooltips, and KPI display - Fix KPI suffix to only append % when value is actually in 0–1 range - Add course_enrollments table to LLM prompt with schema, routing rules, DFWI/pass rate SQL patterns, FERPA guardrails, and worked example - Add SchemaEntry interface; remove as-any cast on courseColumns - SQL patterns return 0–1 scale; display layer handles multiplication * fix: prevent LLM from adding institution_id filter to course_enrollments course_enrollments has no institution_id column; the INSTITUTION context in the prompt was causing the LLM to borrow the Institution_ID filter from the student table and apply it to course queries, producing: Error: column "institution_id" does not exist * feat: add viz type tab strip for user override of LLM chart choice Renders Table/Bar/Line/Pie/KPI buttons above query results. Defaults to the LLM's chosen vizType and resets on each new query, so users can switch views without re-running the query.
1 parent 1472a63 commit 6aeb82d

54 files changed

Lines changed: 7808 additions & 275 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci-dashboard.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Dashboard CI
2+
3+
on:
4+
push:
5+
branches: [main, rebranding/bishop-state]
6+
paths:
7+
- "codebenders-dashboard/**"
8+
- ".github/workflows/ci-dashboard.yml"
9+
pull_request:
10+
branches: [main, rebranding/bishop-state]
11+
paths:
12+
- "codebenders-dashboard/**"
13+
- ".github/workflows/ci-dashboard.yml"
14+
15+
defaults:
16+
run:
17+
working-directory: codebenders-dashboard
18+
19+
jobs:
20+
ci:
21+
name: Type check · Lint · Build
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- uses: actions/setup-node@v4
28+
with:
29+
node-version: "20"
30+
31+
- name: Install dependencies
32+
run: npm install
33+
34+
- name: TypeScript type check
35+
run: npx tsc --noEmit
36+
37+
- name: Lint
38+
run: npm run lint
39+
40+
- name: Build
41+
run: npm run build
42+
env:
43+
# Provide placeholder values so the build doesn't fail on missing env assertions.
44+
# API routes and Supabase calls are opt-in at runtime; they are not executed during build.
45+
NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co
46+
NEXT_PUBLIC_SUPABASE_ANON_KEY: placeholder-anon-key
47+
DB_HOST: localhost
48+
DB_PORT: 5432
49+
DB_USER: postgres
50+
DB_PASSWORD: postgres
51+
DB_NAME: postgres

.github/workflows/ci-python.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Python CI
2+
3+
on:
4+
push:
5+
branches: [main, rebranding/bishop-state]
6+
paths:
7+
- "ai_model/**"
8+
- "operations/**"
9+
- "requirements.txt"
10+
- ".github/workflows/ci-python.yml"
11+
pull_request:
12+
branches: [main, rebranding/bishop-state]
13+
paths:
14+
- "ai_model/**"
15+
- "operations/**"
16+
- "requirements.txt"
17+
- ".github/workflows/ci-python.yml"
18+
19+
jobs:
20+
ci:
21+
name: Lint · Deps · Syntax check
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- uses: actions/setup-python@v5
28+
with:
29+
python-version: "3.11"
30+
cache: "pip"
31+
32+
- name: Install ruff
33+
run: pip install ruff
34+
35+
- name: Lint (ruff)
36+
run: ruff check ai_model/ operations/
37+
38+
- name: Install dependencies
39+
run: pip install -r requirements.txt
40+
41+
- name: Syntax check — entry points
42+
run: |
43+
python -m py_compile ai_model/complete_ml_pipeline.py
44+
python -m py_compile operations/db_config.py
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Deploy Preview
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
jobs:
8+
deploy:
9+
name: Vercel preview deployment
10+
runs-on: ubuntu-latest
11+
# Only run when Vercel secrets are configured (skips forks / contributors without access)
12+
if: ${{ vars.VERCEL_PROJECT_ID != '' }}
13+
14+
permissions:
15+
pull-requests: write # needed to post the preview URL comment
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: "20"
23+
24+
- name: Install Vercel CLI
25+
run: npm install --global vercel@latest
26+
27+
- name: Pull Vercel environment (preview)
28+
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
29+
env:
30+
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
31+
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
32+
33+
- name: Build
34+
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
35+
env:
36+
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
37+
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
38+
39+
- name: Deploy
40+
id: deploy
41+
run: |
42+
url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
43+
echo "url=$url" >> "$GITHUB_OUTPUT"
44+
env:
45+
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
46+
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
47+
48+
- name: Post preview URL to PR
49+
uses: actions/github-script@v7
50+
with:
51+
script: |
52+
github.rest.issues.createComment({
53+
issue_number: context.issue.number,
54+
owner: context.repo.owner,
55+
repo: context.repo.repo,
56+
body: `## Preview deployment\n\n🚀 **${{ steps.deploy.outputs.url }}**\n\nDeployed from ${context.sha.slice(0, 7)}.`,
57+
})
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Deploy Production
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
jobs:
8+
deploy:
9+
name: Vercel production deployment
10+
runs-on: ubuntu-latest
11+
# Only run when Vercel secrets are configured
12+
if: ${{ vars.VERCEL_PROJECT_ID != '' }}
13+
14+
environment:
15+
name: production
16+
url: ${{ steps.deploy.outputs.url }}
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- uses: actions/setup-node@v4
22+
with:
23+
node-version: "20"
24+
25+
- name: Install Vercel CLI
26+
run: npm install --global vercel@latest
27+
28+
- name: Pull Vercel environment (production)
29+
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
30+
env:
31+
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
32+
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
33+
34+
- name: Build
35+
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
36+
env:
37+
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
38+
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
39+
40+
- name: Deploy
41+
id: deploy
42+
run: |
43+
url=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }})
44+
echo "url=$url" >> "$GITHUB_OUTPUT"
45+
env:
46+
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
47+
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Security Audit
2+
3+
on:
4+
schedule:
5+
- cron: "0 9 * * 1" # Every Monday at 09:00 UTC
6+
push:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
jobs:
11+
audit-npm:
12+
name: npm audit (dashboard)
13+
runs-on: ubuntu-latest
14+
15+
defaults:
16+
run:
17+
working-directory: codebenders-dashboard
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: "20"
25+
26+
- name: Install dependencies
27+
run: npm install
28+
29+
- name: Audit
30+
run: npm audit --audit-level=high
31+
32+
audit-python:
33+
name: pip-audit (ML pipeline)
34+
runs-on: ubuntu-latest
35+
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- uses: actions/setup-python@v5
40+
with:
41+
python-version: "3.11"
42+
cache: "pip"
43+
44+
- name: Install pip-audit
45+
run: pip install pip-audit
46+
47+
- name: Audit
48+
run: pip-audit -r requirements.txt

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Desktop.ini
106106
*.csv
107107
*.xlsx
108108
*.xls
109+
*.pptx
109110
*.json
110111
!package.json
111112
!tsconfig.json
@@ -158,6 +159,12 @@ docker-compose.override.yml
158159
# Git worktrees
159160
.worktrees/
160161

162+
# Large presentation/doc files
163+
docs/AI-Powered-Student-Success-Analytics.pptx
164+
docs/Copy-of-AI-Powered-Student-Success-Analytics.pdf
165+
docs/CodeBenders-PRD_Student_Success_Analytics.pdf
166+
DOCUMENTATION_ISSUES.md
167+
161168
# Misc
162169
.cache/
163170
*.seed

ai_model/complete_ml_pipeline.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,17 @@ def assign_credential_type(row):
173173
else:
174174
return 2 # Default to Associate's (most common at community colleges)
175175

176-
# No credential completed
176+
# Priority 5: No completion data — fall back to credential type sought as proxy
177+
# (represents "what credential is this student on track for")
178+
credential_sought = str(row.get('Credential_Type_Sought_Year_1', ''))
179+
if credential_sought in ['01', '02', '03', 'C1', 'C2']:
180+
return 1 # Certificate-track
181+
elif credential_sought in ['A', '04', '05']:
182+
return 2 # Associate-track
183+
elif credential_sought in ['B', '06', '07', '08']:
184+
return 3 # Bachelor-track
185+
186+
# No credential completed or sought
177187
return 0 # No credential
178188

179189
df['target_credential_type'] = df.apply(assign_credential_type, axis=1)
@@ -646,6 +656,7 @@ def assign_alert_level(risk_score):
646656
n_estimators=50,
647657
max_depth=5,
648658
min_samples_split=30,
659+
class_weight='balanced',
649660
random_state=42,
650661
n_jobs=-1
651662
)
@@ -734,7 +745,7 @@ def assign_alert_level(risk_score):
734745
# Only include students who attempted gateway math (not NaN)
735746
gateway_math_raw = df['CompletedGatewayMathYear1']
736747
valid_idx = gateway_math_raw.notna()
737-
y_gateway_math = (gateway_math_raw[valid_idx] == 'C').astype(int)
748+
y_gateway_math = (gateway_math_raw[valid_idx] == 'Y').astype(int)
738749
X_gateway_math = X_gateway_math_clean[valid_idx]
739750

740751
print(f"\nDataset size: {len(X_gateway_math):,} students")
@@ -845,7 +856,7 @@ def assign_alert_level(risk_score):
845856
# Only include students who attempted gateway English (not NaN)
846857
gateway_english_raw = df['CompletedGatewayEnglishYear1']
847858
valid_idx = gateway_english_raw.notna()
848-
y_gateway_english = (gateway_english_raw[valid_idx] == 'C').astype(int)
859+
y_gateway_english = (gateway_english_raw[valid_idx] == 'Y').astype(int)
849860
X_gateway_english = X_gateway_english_clean[valid_idx]
850861

851862
print(f"\nDataset size: {len(X_gateway_english):,} students")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use server"
2+
3+
import { createClient } from "@/lib/supabase/server"
4+
import { redirect } from "next/navigation"
5+
6+
export async function signOut() {
7+
const supabase = await createClient()
8+
await supabase.auth.signOut()
9+
redirect("/login")
10+
}

0 commit comments

Comments
 (0)