Skip to content
Merged
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
117 changes: 117 additions & 0 deletions codebenders-dashboard/app/glossary/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import Link from "next/link"
import {
buildGlossaryTopicBlocks,
formatGlossarySlugForDisplay,
parseGlossaryEntries,
readMetricGlossaryMarkdown,
} from "@/lib/metric-glossary"

const BOLD_LEAD_PARAGRAPH = /^\*\*([^*]+)\*\*:\s*([\s\S]*)$/

function GlossaryBody({ text }: { text: string }) {
const paragraphs = text.split(/\n\n+/).filter(Boolean)
return (
<div className="space-y-3 text-sm text-muted-foreground">
{paragraphs.map((p, i) => {
const boldLead = BOLD_LEAD_PARAGRAPH.exec(p)
if (boldLead) {
return (
<p key={i}>
<span className="font-medium text-foreground">{boldLead[1]}:</span> {boldLead[2]}
</p>
)
}
return <p key={i}>{p}</p>
})}
</div>
)
}

export default function GlossaryPage() {
const md = readMetricGlossaryMarkdown()
const entries = parseGlossaryEntries(md)
const topicBlocks = buildGlossaryTopicBlocks(entries)
const alphaSlugs = Object.keys(entries).sort((a, b) => a.localeCompare(b))

return (
<div className="min-h-screen bg-background">
<div className="container mx-auto max-w-4xl p-6 space-y-10">
<header className="space-y-2 border-b border-border pb-6">
<h1 className="text-3xl font-bold tracking-tight">Metric glossary</h1>
<p className="text-muted-foreground">
In-context definitions for dashboard KPIs, with PDP field notes and high-level IPEDS / state
cross-walks. Source:{" "}
<code className="text-xs bg-muted px-1 py-0.5 rounded">content/metric-glossary.md</code>
</p>
<p className="text-sm text-muted-foreground">
<Link href="/" className="text-primary underline-offset-4 hover:underline">
Back to dashboard
</Link>
{" · "}
<Link href="/methodology" className="text-primary underline-offset-4 hover:underline">
Methodology
</Link>
</p>
</header>

<nav aria-label="On this page" className="rounded-lg border border-border bg-muted/30 p-4 space-y-3">
<h2 className="text-sm font-semibold text-foreground">By topic</h2>
<ul className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
{topicBlocks.map((t) => (
<li key={t.id}>
<a href={`#topic-${t.id}`} className="text-primary underline-offset-4 hover:underline">
{t.label}
</a>
</li>
))}
</ul>
<h2 className="text-sm font-semibold text-foreground pt-2">A–Z</h2>
<ul className="flex flex-wrap gap-x-3 gap-y-1 text-xs font-mono text-muted-foreground">
{alphaSlugs.map((slug) => (
<li key={slug}>
<a href={`#${slug}`} className="hover:text-foreground underline-offset-2 hover:underline">
{formatGlossarySlugForDisplay(slug)}
</a>
</li>
))}
</ul>
</nav>

{topicBlocks.map((topic) => (
<section key={topic.id} id={`topic-${topic.id}`} className="space-y-6 scroll-mt-20">
<h2 className="text-xl font-semibold tracking-tight border-b border-border pb-2">{topic.label}</h2>
{topic.slugs.map((slug) => {
const body = entries[slug]!
const title = formatGlossarySlugForDisplay(slug)
return (
<article key={slug} id={slug} className="scroll-mt-20 space-y-2 rounded-lg border border-border p-5">
<h3 className="text-lg font-medium capitalize">{title}</h3>
<GlossaryBody text={body} />
</article>
)
})}
</section>
))}

<footer className="text-xs text-muted-foreground border-t border-border pt-6">
<p>
Epic tracking:{" "}
<a
href="https://github.com/devcolor/codebenders-datathon/issues/124"
className="text-primary underline-offset-4 hover:underline"
>
#124 AASCU convening follow-ups
</a>
{" · "}Issue{" "}
<a
href="https://github.com/devcolor/codebenders-datathon/issues/105"
className="text-primary underline-offset-4 hover:underline"
>
#105
</a>
</p>
</footer>
</div>
</div>
)
}
21 changes: 21 additions & 0 deletions codebenders-dashboard/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "@/components/ui/select"
import { TrendingUp, Users, AlertTriangle, BookOpen, Search, Table2, X } from "lucide-react"
import Link from "next/link"
import { GLOSSARY_HREF } from "@/lib/glossary-constants"

interface KPIData {
overallRetentionRate: string
Expand Down Expand Up @@ -275,6 +276,11 @@ export default function DashboardPage() {
<p><strong>What it shows:</strong> Percentage of students retained year-to-year based on historical data.</p>
<p className="mt-2"><strong>Data source:</strong> Retention field from student cohort records (0=Not Retained, 1=Retained).</p>
<p className="mt-2"><strong>Use for:</strong> Baseline institutional performance metric.</p>
<p className="mt-3">
<Link href={`${GLOSSARY_HREF}#overall-retention-rate`} className="text-primary underline-offset-4 hover:underline text-xs font-medium">
Full glossary entry (PDP / IPEDS cross-walk) →
</Link>
</p>
</>
}
/>
Expand All @@ -295,6 +301,11 @@ export default function DashboardPage() {
<li>First Year GPA (2.9%)</li>
</ul>
<p className="mt-2"><strong>Use for:</strong> Early identification of at-risk students for proactive intervention.</p>
<p className="mt-3">
<Link href={`${GLOSSARY_HREF}#avg-predicted-retention`} className="text-primary underline-offset-4 hover:underline text-xs font-medium">
Full glossary entry (PDP / IPEDS cross-walk) →
</Link>
</p>
</>
}
/>
Expand All @@ -319,6 +330,11 @@ export default function DashboardPage() {
<li><strong>HIGH:</strong> Priority intervention</li>
</ul>
<p className="mt-2"><strong>Recommended actions:</strong> Immediate advisor outreach, financial aid review, tutoring referrals.</p>
<p className="mt-3">
<Link href={`${GLOSSARY_HREF}#students-at-high-critical-risk`} className="text-primary underline-offset-4 hover:underline text-xs font-medium">
Full glossary entry (PDP / IPEDS cross-walk) →
</Link>
</p>
</>
}
/>
Expand All @@ -339,6 +355,11 @@ export default function DashboardPage() {
<li>&lt;50%: Critical - failing nearly half of courses</li>
</ul>
<p className="mt-2"><strong>Why it matters:</strong> Strong predictor of retention and credential completion.</p>
<p className="mt-3">
<Link href={`${GLOSSARY_HREF}#avg-course-completion`} className="text-primary underline-offset-4 hover:underline text-xs font-medium">
Full glossary entry (PDP / IPEDS cross-walk) →
</Link>
</p>
</>
}
/>
Expand Down
2 changes: 2 additions & 0 deletions codebenders-dashboard/components/nav-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { GraduationCap, LogOut } from "lucide-react"
import { Button } from "@/components/ui/button"
import { signOut } from "@/app/actions/auth"
import { AI_TRANSPARENCY_HREF } from "@/content/ai-transparency"
import { GLOSSARY_HREF } from "@/lib/glossary-constants"
import { ROLE_COLORS, ROLE_LABELS, type Role } from "@/lib/roles"

interface NavHeaderProps {
Expand All @@ -15,6 +16,7 @@ interface NavHeaderProps {

const NAV_LINKS: Array<{ href: string; label: string; roles?: Role[] }> = [
{ href: "/", label: "Dashboard" },
{ href: GLOSSARY_HREF, label: "Glossary" },
{ href: "/courses", label: "Courses" },
{ href: "/students", label: "Students" },
{ href: "/query", label: "Query" },
Expand Down
51 changes: 51 additions & 0 deletions codebenders-dashboard/content/metric-glossary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Metric glossary

Single source of truth for dashboard KPI definitions. Each section is keyed by its URL anchor (e.g. `/glossary#overall-retention-rate`). Cross-walks are indicative — institutions should confirm against their PDP documentation, IPEDS submission manuals, and state reporting rules.

---

## overall-retention-rate

**Plain English:** Share of students in the selected cohort who are still enrolled (or completed) one year later — a common “year-to-year” retention view for the students you are filtering.

**PDP / analysis-ready:** Uses the cohort retention indicator on `student_level_with_predictions` (historical `Retention` field: 0 = not retained to the next year, 1 = retained) after filters (cohort, enrollment intensity, credential goal) are applied.

**IPEDS:** Closest published analog is *Fall cohort retention* for first-time full-time students; PDP cohorts may include part-time or mixed populations, so percentages will not match IPEDS one-to-one without aligning cohort definitions.

**State compliance:** Many state accountability dashboards publish “first-year retention” or “persistence”; map this metric to the state field that uses the same cohort and time window.

---

## avg-predicted-retention

**Plain English:** Average of the model’s estimated probability (0–100%) that each student in the filtered set will retain — not the same as historical retention above.

**PDP / analysis-ready:** Mean of `retention_probability` from the deployed XGBoost retention model on `student_level_with_predictions`.

**IPEDS:** No direct IPEDS submission — this is an institutional analytics prediction, not an audited outcome count.

**State compliance:** Treat as early-warning / planning metric unless your state explicitly allows predictive indicators in reporting.

---

## students-at-high-critical-risk

**Plain English:** Count of students whose composite risk score places them in the **HIGH** or **URGENT** alert bands used for intervention triage.

**PDP / analysis-ready:** Derived from `at_risk_alert` and related thresholds on `student_level_with_predictions` (see dashboard methodology for the composite formula).

**IPEDS:** Not an IPEDS field; comparable to internal early-alert counts only.

**State compliance:** Use for operations; confirm before exporting small subgroup counts externally (FERPA / small-N policies).

---

## avg-course-completion

**Plain English:** Credits successfully completed divided by credits attempted, expressed as a percentage, aggregated across students in the filter.

**PDP / analysis-ready:** Computed from course completion fields on `student_level_with_predictions` (credits attempted vs. earned in the modeled year window).

**IPEDS:** Conceptually related to success rates and progression, but IPEDS collects many distinct measures (e.g., completions by award) — do not assume identity without a written cross-walk.

**State compliance:** Often parallels “success rate” or “credit completion ratio” in performance funding models; verify denominator rules match your state formula.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest"
import {
DASHBOARD_KPI_GLOSSARY_SLUGS,
GLOSSARY_TOPIC_SECTIONS,
} from "@/lib/glossary-constants"
import { parseGlossaryEntries, readMetricGlossaryMarkdown } from "@/lib/metric-glossary"

describe("metric glossary coverage (#105)", () => {
it("includes every dashboard KPI slug in metric-glossary.md", () => {
const md = readMetricGlossaryMarkdown()
const entries = parseGlossaryEntries(md)
for (const slug of DASHBOARD_KPI_GLOSSARY_SLUGS) {
expect(entries[slug], `missing ## ${slug} in content/metric-glossary.md`).toBeTruthy()
expect(entries[slug]!.length).toBeGreaterThan(20)
}
})

it("topic sections list each KPI slug exactly once", () => {
const listed = GLOSSARY_TOPIC_SECTIONS.flatMap((t) => [...t.slugOrder])
expect(listed.length).toBe(DASHBOARD_KPI_GLOSSARY_SLUGS.length)
for (const slug of DASHBOARD_KPI_GLOSSARY_SLUGS) {
const n = listed.filter((s) => s === slug).length
expect(n, `slug ${slug} should appear once in GLOSSARY_TOPIC_SECTIONS`).toBe(1)
}
})
})
42 changes: 42 additions & 0 deletions codebenders-dashboard/lib/glossary-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/** Nav and deep links — safe to import from client components. */

export const GLOSSARY_HREF = "/glossary" as const

/**
* Source of truth for topic groupings and KPI slug order on the glossary page.
* `DASHBOARD_KPI_GLOSSARY_SLUGS` is derived from this list; every `##` section in
* `content/metric-glossary.md` must stay in sync (see
* `lib/__tests__/metric-glossary-coverage.test.ts`).
*/
const GLOSSARY_TOPIC_SECTIONS_RAW = [
{
id: "retention",
label: "Retention, risk & predictions",
slugOrder: [
"overall-retention-rate",
"avg-predicted-retention",
"students-at-high-critical-risk",
] as const,
},
{
id: "completion",
label: "Completion & course success",
slugOrder: ["avg-course-completion"] as const,
},
] as const

export type DashboardKpiGlossarySlug =
(typeof GLOSSARY_TOPIC_SECTIONS_RAW)[number]["slugOrder"][number]

export const DASHBOARD_KPI_GLOSSARY_SLUGS: readonly DashboardKpiGlossarySlug[] =
GLOSSARY_TOPIC_SECTIONS_RAW.flatMap((topic) => [...topic.slugOrder])

export const GLOSSARY_TOPIC_SECTIONS: {
id: string
label: string
slugOrder: readonly DashboardKpiGlossarySlug[]
}[] = GLOSSARY_TOPIC_SECTIONS_RAW.map((topic) => ({
id: topic.id,
label: topic.label,
slugOrder: topic.slugOrder,
}))
49 changes: 49 additions & 0 deletions codebenders-dashboard/lib/metric-glossary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import fs from "fs"
import path from "path"

import { GLOSSARY_TOPIC_SECTIONS } from "@/lib/glossary-constants"

export function readMetricGlossaryMarkdown(): string {
const file = path.join(process.cwd(), "content", "metric-glossary.md")
return fs.readFileSync(file, "utf8")
}

/** Map slug → markdown body (text below `## slug`). */
export function parseGlossaryEntries(md: string): Record<string, string> {
const out: Record<string, string> = {}
const lines = md.split(/\n/)
let current: string | null = null
const buf: string[] = []

const flush = (): void => {
if (current) out[current] = buf.join("\n").trim()
buf.length = 0
}

for (const line of lines) {
const m = /^## ([a-z0-9-]+)\s*$/.exec(line)
if (m) {
flush()
current = m[1]
} else if (current) {
buf.push(line)
}
}
flush()
return out
}

export function formatGlossarySlugForDisplay(slug: string): string {
return slug.replace(/-/g, " ")
}

export function buildGlossaryTopicBlocks(
entries: Record<string, string>
): { id: string; label: string; slugs: string[] }[] {
const present = new Set(Object.keys(entries))
return GLOSSARY_TOPIC_SECTIONS.map((topic) => ({
id: topic.id,
label: topic.label,
slugs: topic.slugOrder.filter((slug) => present.has(slug)),
}))
}
Loading