mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 01:47:41 +00:00
1106 lines
38 KiB
HTML
1106 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>WIN — Architecture Overview</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, sans-serif;
|
|
background: #f5f7fa;
|
|
color: #111827;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
header {
|
|
background: #111827;
|
|
color: white;
|
|
padding: 2.5rem 1.5rem;
|
|
text-align: center;
|
|
}
|
|
header h1 { font-size: 2rem; margin-bottom: .4rem; }
|
|
header p { opacity: .8; max-width: 60ch; margin: 0 auto; font-size: .95rem; }
|
|
|
|
nav {
|
|
background: #1e2a3a;
|
|
display: flex;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
gap: .25rem;
|
|
padding: .6rem 1rem;
|
|
}
|
|
nav a {
|
|
color: #93c5fd;
|
|
text-decoration: none;
|
|
font-size: .82rem;
|
|
padding: .3rem .7rem;
|
|
border-radius: 6px;
|
|
transition: background .15s;
|
|
}
|
|
nav a:hover { background: rgba(255,255,255,.08); color: white; }
|
|
|
|
main {
|
|
max-width: 1160px;
|
|
margin: 0 auto;
|
|
padding: 1.5rem 1rem 4rem;
|
|
}
|
|
|
|
/* ── section headings ───────────────────────────────────── */
|
|
.section-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
color: #111827;
|
|
margin: 2.5rem 0 1rem;
|
|
padding-bottom: .4rem;
|
|
border-bottom: 2px solid #e5e7eb;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .5rem;
|
|
}
|
|
.section-title span { font-size: 1.2rem; }
|
|
|
|
/* ── tier diagram ───────────────────────────────────────── */
|
|
.arch-diagram {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.tier {
|
|
display: flex;
|
|
align-items: stretch;
|
|
gap: .75rem;
|
|
position: relative;
|
|
}
|
|
|
|
.tier-label {
|
|
writing-mode: vertical-rl;
|
|
text-orientation: mixed;
|
|
transform: rotate(180deg);
|
|
font-size: .7rem;
|
|
font-weight: 700;
|
|
letter-spacing: .08em;
|
|
text-transform: uppercase;
|
|
color: white;
|
|
padding: .5rem .35rem;
|
|
border-radius: 8px 0 0 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 28px;
|
|
}
|
|
|
|
.tier-body {
|
|
flex: 1;
|
|
display: flex;
|
|
gap: .75rem;
|
|
flex-wrap: wrap;
|
|
padding: .9rem;
|
|
border-radius: 0 12px 12px 0;
|
|
}
|
|
|
|
/* tier colour themes */
|
|
.tier--client .tier-label { background: #2563eb; }
|
|
.tier--client .tier-body { background: #eff6ff; border: 1.5px solid #bfdbfe; }
|
|
|
|
.tier--proxy .tier-label { background: #64748b; }
|
|
.tier--proxy .tier-body { background: #f8fafc; border: 1.5px solid #cbd5e1; }
|
|
|
|
.tier--api .tier-label { background: #16a34a; }
|
|
.tier--api .tier-body { background: #f0fdf4; border: 1.5px solid #bbf7d0; }
|
|
|
|
.tier--data .tier-label { background: #d97706; }
|
|
.tier--data .tier-body { background: #fffbeb; border: 1.5px solid #fde68a; }
|
|
|
|
.tier--ext .tier-label { background: #7c3aed; }
|
|
.tier--ext .tier-body { background: #faf5ff; border: 1.5px solid #ddd6fe; }
|
|
|
|
/* arrow between tiers */
|
|
.tier-arrow {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 30px;
|
|
gap: .4rem;
|
|
color: #6b7280;
|
|
font-size: .72rem;
|
|
margin-left: 28px; /* align with tier-body */
|
|
}
|
|
.tier-arrow svg { flex-shrink: 0; }
|
|
|
|
/* ── boxes inside tiers ─────────────────────────────────── */
|
|
.box {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: .6rem .85rem;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,.07);
|
|
border: 1px solid rgba(0,0,0,.06);
|
|
min-width: 130px;
|
|
}
|
|
.box-title {
|
|
font-size: .8rem;
|
|
font-weight: 700;
|
|
margin-bottom: .3rem;
|
|
}
|
|
.box ul {
|
|
list-style: none;
|
|
font-size: .72rem;
|
|
color: #4b5563;
|
|
}
|
|
.box ul li { margin-bottom: .15rem; }
|
|
.box ul li::before { content: "• "; color: #9ca3af; }
|
|
|
|
.box--blue { border-top: 3px solid #2563eb; }
|
|
.box--green { border-top: 3px solid #16a34a; }
|
|
.box--amber { border-top: 3px solid #d97706; }
|
|
.box--purple { border-top: 3px solid #7c3aed; }
|
|
.box--slate { border-top: 3px solid #64748b; }
|
|
.box--rose { border-top: 3px solid #e11d48; }
|
|
.box--teal { border-top: 3px solid #0d9488; }
|
|
.box--indigo { border-top: 3px solid #4f46e5; }
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
font-size: .65rem;
|
|
font-weight: 700;
|
|
padding: .1rem .4rem;
|
|
border-radius: 4px;
|
|
margin-right: .25rem;
|
|
vertical-align: middle;
|
|
}
|
|
.badge--blue { background: #dbeafe; color: #1d4ed8; }
|
|
.badge--green { background: #dcfce7; color: #15803d; }
|
|
.badge--amber { background: #fef3c7; color: #b45309; }
|
|
.badge--purple { background: #ede9fe; color: #6d28d9; }
|
|
|
|
/* ── auth flow ──────────────────────────────────────────── */
|
|
.flow {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0;
|
|
overflow-x: auto;
|
|
padding-bottom: .5rem;
|
|
}
|
|
.flow-step {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
min-width: 150px;
|
|
max-width: 170px;
|
|
}
|
|
.flow-bubble {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: .6rem .75rem;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,.08);
|
|
border: 1.5px solid #e5e7eb;
|
|
font-size: .72rem;
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
.flow-bubble strong { display: block; font-size: .8rem; margin-bottom: .25rem; color: #111827; }
|
|
.flow-bubble .note { color: #6b7280; margin-top: .25rem; font-style: italic; }
|
|
.flow-num {
|
|
width: 22px; height: 22px;
|
|
border-radius: 50%;
|
|
background: #111827;
|
|
color: white;
|
|
font-size: .72rem;
|
|
font-weight: 700;
|
|
display: flex; align-items: center; justify-content: center;
|
|
margin-bottom: .4rem;
|
|
}
|
|
.flow-connector {
|
|
display: flex;
|
|
align-items: center;
|
|
padding-top: 11px; /* align with bubble mid */
|
|
flex-shrink: 0;
|
|
}
|
|
.flow-connector svg { color: #9ca3af; }
|
|
.flow-connector span { font-size: .65rem; color: #9ca3af; white-space: nowrap; padding: 0 .3rem; }
|
|
|
|
/* ── data model ─────────────────────────────────────────── */
|
|
.entity-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: .75rem;
|
|
}
|
|
.entity {
|
|
background: white;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,.07);
|
|
border: 1px solid #e5e7eb;
|
|
overflow: hidden;
|
|
}
|
|
.entity-header {
|
|
padding: .45rem .75rem;
|
|
font-size: .78rem;
|
|
font-weight: 700;
|
|
color: white;
|
|
}
|
|
.entity-fields {
|
|
padding: .5rem .75rem;
|
|
font-size: .7rem;
|
|
color: #4b5563;
|
|
}
|
|
.entity-fields div { padding: .1rem 0; border-bottom: 1px solid #f3f4f6; }
|
|
.entity-fields div:last-child { border-bottom: none; }
|
|
.pk { color: #d97706; font-weight: 700; }
|
|
.fk { color: #2563eb; }
|
|
.field-note { color: #9ca3af; font-size: .65rem; margin-left: .25rem; }
|
|
|
|
.rel-diagram {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
/* ── roles ──────────────────────────────────────────────── */
|
|
.role-pyramid {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: .4rem;
|
|
padding: .5rem 0;
|
|
}
|
|
.role-row {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
.role-chip {
|
|
border-radius: 8px;
|
|
padding: .5rem 1.2rem;
|
|
font-size: .78rem;
|
|
font-weight: 700;
|
|
color: white;
|
|
text-align: center;
|
|
min-width: 160px;
|
|
}
|
|
|
|
/* ── permission matrix ──────────────────────────────────── */
|
|
table.matrix {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: .72rem;
|
|
}
|
|
table.matrix th, table.matrix td {
|
|
border: 1px solid #e5e7eb;
|
|
padding: .35rem .5rem;
|
|
text-align: center;
|
|
}
|
|
table.matrix th { background: #f9fafb; font-weight: 700; color: #374151; }
|
|
table.matrix td:first-child { text-align: left; font-weight: 600; color: #374151; }
|
|
.allow { background: #dcfce7; color: #15803d; font-weight: 700; border-radius: 4px; padding: .1rem .3rem; font-size: .65rem; }
|
|
.mine { background: #fef3c7; color: #b45309; font-weight: 700; border-radius: 4px; padding: .1rem .3rem; font-size: .65rem; }
|
|
.deny { background: #fee2e2; color: #b91c1c; font-weight: 700; border-radius: 4px; padding: .1rem .3rem; font-size: .65rem; }
|
|
|
|
/* ── cards row ──────────────────────────────────────────── */
|
|
.info-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
gap: .75rem;
|
|
}
|
|
.info-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 1rem 1.1rem;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,.07);
|
|
border: 1px solid #e5e7eb;
|
|
}
|
|
.info-card h3 { font-size: .85rem; font-weight: 700; margin-bottom: .5rem; color: #111827; }
|
|
.info-card ul { list-style: none; font-size: .75rem; color: #4b5563; }
|
|
.info-card ul li { padding: .2rem 0; border-bottom: 1px solid #f3f4f6; }
|
|
.info-card ul li:last-child { border-bottom: none; }
|
|
.info-card ul li::before { content: "→ "; color: #9ca3af; }
|
|
|
|
/* ── footer ─────────────────────────────────────────────── */
|
|
footer {
|
|
text-align: center;
|
|
font-size: .75rem;
|
|
color: #9ca3af;
|
|
padding: 2rem 1rem;
|
|
border-top: 1px solid #e5e7eb;
|
|
margin-top: 3rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>WIN — Architectural Overview</h1>
|
|
<p>High-level diagram of the system tiers, components, data model, auth flow, and role hierarchy.</p>
|
|
</header>
|
|
|
|
<nav>
|
|
<a href="index.html">Home</a>
|
|
<a href="technical.html">← Technical Docs</a>
|
|
<a href="api.html">API Docs</a>
|
|
<a href="db.html">Database</a>
|
|
<a href="erd.html">ERD</a>
|
|
<a href="um.html">User Manual</a>
|
|
</nav>
|
|
|
|
<main>
|
|
|
|
<!-- ══════════════════════════════════════════
|
|
1. SYSTEM TIER DIAGRAM
|
|
══════════════════════════════════════════ -->
|
|
<div class="section-title"><span>🏗</span> System Architecture</div>
|
|
|
|
<div class="arch-diagram">
|
|
|
|
<!-- CLIENT TIER -->
|
|
<div class="tier tier--client">
|
|
<div class="tier-label">Client</div>
|
|
<div class="tier-body">
|
|
|
|
<div class="box box--blue">
|
|
<div class="box-title">Angular SPA <span class="badge badge--blue">v20</span></div>
|
|
<ul>
|
|
<li>TypeScript 5.8 + SCSS</li>
|
|
<li>Angular Signals (state)</li>
|
|
<li>Lazy-loaded routes</li>
|
|
<li>JWT auth interceptor</li>
|
|
<li>Proactive token refresh</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="box box--blue">
|
|
<div class="box-title">Desktop Layout</div>
|
|
<ul>
|
|
<li>Home / Workspace</li>
|
|
<li>Student detail view</li>
|
|
<li>Goal & Benchmark modals</li>
|
|
<li>Progress event modals</li>
|
|
<li>Report generation UI</li>
|
|
<li>Admin panel</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="box box--blue">
|
|
<div class="box-title">Mobile Layout</div>
|
|
<ul>
|
|
<li>Student card list</li>
|
|
<li>Add progress event</li>
|
|
<li>Toggle benchmark</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="box box--blue">
|
|
<div class="box-title">Shared Services</div>
|
|
<ul>
|
|
<li>AuthService (signals)</li>
|
|
<li>ApiService (HTTP)</li>
|
|
<li>StudentService</li>
|
|
<li>AdminService</li>
|
|
<li>ReportPromptService</li>
|
|
<li>PlatformService</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tier-arrow">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 5v14M5 12l7 7 7-7"/>
|
|
</svg>
|
|
<span>HTTPS / REST + JWT Bearer — win.opelly.me → winapi.opelly.me</span>
|
|
</div>
|
|
|
|
<!-- PROXY TIER -->
|
|
<div class="tier tier--proxy">
|
|
<div class="tier-label">Proxy</div>
|
|
<div class="tier-body">
|
|
|
|
<div class="box box--slate">
|
|
<div class="box-title">Traefik Reverse Proxy</div>
|
|
<ul>
|
|
<li>win.opelly.me → Angular (Nginx :8006)</li>
|
|
<li>winapi.opelly.me → API (:8005)</li>
|
|
<li>Let's Encrypt TLS</li>
|
|
<li>Gzip compression</li>
|
|
<li>Security headers</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tier-arrow">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 5v14M5 12l7 7 7-7"/>
|
|
</svg>
|
|
<span>HTTP (internal Docker network)</span>
|
|
</div>
|
|
|
|
<!-- API TIER -->
|
|
<div class="tier tier--api">
|
|
<div class="tier-label">API</div>
|
|
<div class="tier-body">
|
|
|
|
<div class="box box--green">
|
|
<div class="box-title">ASP.NET Core 9 <span class="badge badge--green">C#</span></div>
|
|
<ul>
|
|
<li>JWT authentication middleware</li>
|
|
<li>Swagger / OpenAPI</li>
|
|
<li>CORS policy</li>
|
|
<li>Dependency injection</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="box box--green">
|
|
<div class="box-title">Controllers</div>
|
|
<ul>
|
|
<li>/api/Auth — login & tokens</li>
|
|
<li>/api/Student — CRUD</li>
|
|
<li>/api/Admin — district/users</li>
|
|
<li>/api/ReportPrompt — prompts</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="box box--green">
|
|
<div class="box-title">Services</div>
|
|
<ul>
|
|
<li>TokenService (JWT)</li>
|
|
<li>PermissionService</li>
|
|
<li>PasswordHasher (PBKDF2)</li>
|
|
<li>RecommendationService</li>
|
|
<li>TranscriptionService</li>
|
|
<li>ProgressReportBuilder</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="box box--green">
|
|
<div class="box-title">Repositories <span class="badge badge--green">Dapper</span></div>
|
|
<ul>
|
|
<li>StudentRepository</li>
|
|
<li>UserRepository</li>
|
|
<li>AuthRepository</li>
|
|
<li>AdminRepository</li>
|
|
<li>ReportPromptRepository</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tier-arrow">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 5v14M5 12l7 7 7-7"/>
|
|
</svg>
|
|
<span>Stored procedures via Dapper — TCP :3309 (Docker internal)</span>
|
|
</div>
|
|
|
|
<!-- DATA TIER -->
|
|
<div class="tier tier--data">
|
|
<div class="tier-label">Data</div>
|
|
<div class="tier-body">
|
|
|
|
<div class="box box--amber">
|
|
<div class="box-title">MySQL 8 <span class="badge badge--amber">Docker</span></div>
|
|
<ul>
|
|
<li>winstudentgoaltracker DB</li>
|
|
<li>17 tables</li>
|
|
<li>51 stored procedures</li>
|
|
<li>Initialised from db/docker-init/</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="box box--amber">
|
|
<div class="box-title">Core Tables</div>
|
|
<ul>
|
|
<li>user, school_district, program</li>
|
|
<li>user_program (roles junction)</li>
|
|
<li>student, user_student</li>
|
|
<li>goal (recursive), benchmark</li>
|
|
<li>progress_event + junction</li>
|
|
<li>refresh_token</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="box box--amber">
|
|
<div class="box-title">Procedure Groups</div>
|
|
<ul>
|
|
<li>sp_Student_* (CRUD)</li>
|
|
<li>sp_Goal_* / sp_Benchmark_*</li>
|
|
<li>sp_ProgressEvent_*</li>
|
|
<li>sp_RefreshToken_*</li>
|
|
<li>sp_Program_* / sp_User_*</li>
|
|
<li>sp_ProgressReport_*</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- External services arrow (side) -->
|
|
<div class="tier-arrow">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 5v14M5 12l7 7 7-7"/>
|
|
</svg>
|
|
<span>Outbound HTTP from API tier only</span>
|
|
</div>
|
|
|
|
<!-- EXTERNAL TIER -->
|
|
<div class="tier tier--ext">
|
|
<div class="tier-label">External</div>
|
|
<div class="tier-body">
|
|
|
|
<div class="box box--purple">
|
|
<div class="box-title">Ollama LLM <span class="badge badge--purple">AI</span></div>
|
|
<ul>
|
|
<li>llm.opelly.me</li>
|
|
<li>Model: gemma4:e2b</li>
|
|
<li>Benchmark recommendations</li>
|
|
<li>5-min timeout</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="box box--purple">
|
|
<div class="box-title">STT Service <span class="badge badge--purple">AI</span></div>
|
|
<ul>
|
|
<li>stt.opelly.me</li>
|
|
<li>Speech-to-text transcription</li>
|
|
<li>Progress event dictation</li>
|
|
<li>5-min timeout</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /arch-diagram -->
|
|
|
|
|
|
<!-- ══════════════════════════════════════════
|
|
2. AUTH FLOW
|
|
══════════════════════════════════════════ -->
|
|
<div class="section-title"><span>🔐</span> Two-Phase Authentication Flow</div>
|
|
|
|
<div style="overflow-x:auto; padding-bottom:.5rem;">
|
|
<div class="flow" style="min-width:900px;">
|
|
|
|
<div class="flow-step">
|
|
<div class="flow-num">1</div>
|
|
<div class="flow-bubble">
|
|
<strong>User Submits Credentials</strong>
|
|
email + password<br>
|
|
<span class="note">POST /api/Auth/Login</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flow-connector">
|
|
<svg width="28" height="16" viewBox="0 0 28 16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M0 8h24M18 2l6 6-6 6"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="flow-step">
|
|
<div class="flow-num">2</div>
|
|
<div class="flow-bubble">
|
|
<strong>Phase 1 Response</strong>
|
|
Session token (5 min)<br>+ program list<br>
|
|
<span class="note">Stored in localStorage</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flow-connector">
|
|
<svg width="28" height="16" viewBox="0 0 28 16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M0 8h24M18 2l6 6-6 6"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="flow-step">
|
|
<div class="flow-num">3</div>
|
|
<div class="flow-bubble">
|
|
<strong>User Selects Program</strong>
|
|
session token + program ID<br>
|
|
<span class="note">POST /api/Auth/SelectProgram</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flow-connector">
|
|
<svg width="28" height="16" viewBox="0 0 28 16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M0 8h24M18 2l6 6-6 6"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="flow-step">
|
|
<div class="flow-num">4</div>
|
|
<div class="flow-bubble">
|
|
<strong>Phase 2 Response</strong>
|
|
JWT (1 min) +<br>Refresh token (30 days)<br>
|
|
<span class="note">JWT includes role + program_id</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flow-connector">
|
|
<svg width="28" height="16" viewBox="0 0 28 16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M0 8h24M18 2l6 6-6 6"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="flow-step">
|
|
<div class="flow-num">5</div>
|
|
<div class="flow-bubble">
|
|
<strong>Authenticated Requests</strong>
|
|
Bearer JWT on all API calls<br>
|
|
<span class="note">Auth interceptor injects token</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flow-connector">
|
|
<svg width="28" height="16" viewBox="0 0 28 16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M0 8h24M18 2l6 6-6 6"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="flow-step">
|
|
<div class="flow-num">6</div>
|
|
<div class="flow-bubble">
|
|
<strong>Proactive Refresh</strong>
|
|
10 s before JWT expiry:<br>POST /api/Auth/RefreshToken<br>
|
|
<span class="note">Rotated refresh token returned</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top:.75rem; font-size:.75rem; color:#6b7280; background:white; border-radius:10px; padding:.75rem 1rem; border:1px solid #e5e7eb;">
|
|
<strong>Token storage:</strong> <code>auth_jwt</code>, <code>auth_refresh_token</code>, <code>auth_session_token</code> — all in <code>localStorage</code>.
|
|
|
|
|
<strong>401 interceptor:</strong> attempts one silent refresh; if that fails, redirects to <code>/login</code>.
|
|
|
|
|
<strong>Refresh token rotation:</strong> each use replaces the old token (tracked via <code>replaced_by_token_id</code>).
|
|
</div>
|
|
|
|
|
|
<!-- ══════════════════════════════════════════
|
|
3. DATA MODEL
|
|
══════════════════════════════════════════ -->
|
|
<div class="section-title"><span>🗄</span> Core Data Model</div>
|
|
|
|
<div class="entity-grid">
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#64748b;">school_district</div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_school_district</div>
|
|
<div>name</div>
|
|
<div>contact_email</div>
|
|
<div class="field-note" style="color:#9ca3af;font-size:.68rem;padding:.2rem 0;">Top-level tenant</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#2563eb;">program</div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_program</div>
|
|
<div><span class="fk">FK</span> id_school_district</div>
|
|
<div>name, description</div>
|
|
<div class="field-note">Scope boundary for data</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#0d9488;">user</div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_user</div>
|
|
<div>email, name</div>
|
|
<div>password_hash, password_salt</div>
|
|
<div>locked_until</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#7c3aed;">user_program <span style="font-size:.65rem;opacity:.8;">(junction)</span></div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_user_program</div>
|
|
<div><span class="fk">FK</span> id_user</div>
|
|
<div><span class="fk">FK</span> id_program</div>
|
|
<div><span class="fk">FK</span> id_role</div>
|
|
<div>is_primary, status</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#d97706;">student</div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_student</div>
|
|
<div><span class="fk">FK</span> id_program</div>
|
|
<div>identifier, program_year</div>
|
|
<div>enrollment_date</div>
|
|
<div>next_iep_date</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#7c3aed;">user_student <span style="font-size:.65rem;opacity:.8;">(junction)</span></div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_user_student</div>
|
|
<div><span class="fk">FK</span> id_user</div>
|
|
<div><span class="fk">FK</span> id_student</div>
|
|
<div>is_primary</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#16a34a;">goal</div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_goal</div>
|
|
<div><span class="fk">FK</span> id_goal_parent <span class="field-note">(self-ref)</span></div>
|
|
<div><span class="fk">FK</span> id_student</div>
|
|
<div>description, category</div>
|
|
<div>baseline, target_completion_date</div>
|
|
<div>close_date, achieved</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#0d9488;">benchmark</div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_benchmark</div>
|
|
<div><span class="fk">FK</span> id_goal</div>
|
|
<div>benchmark (description)</div>
|
|
<div>short_name</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#e11d48;">progress_event</div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_progress_event</div>
|
|
<div><span class="fk">FK</span> id_goal</div>
|
|
<div><span class="fk">FK</span> id_user_created</div>
|
|
<div>content (rich text)</div>
|
|
<div>is_sensitive</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#7c3aed;">progress_event_benchmark <span style="font-size:.65rem;opacity:.8;">(junction)</span></div>
|
|
<div class="entity-fields">
|
|
<div><span class="fk">FK</span> id_progress_event</div>
|
|
<div><span class="fk">FK</span> id_benchmark</div>
|
|
<div class="field-note">Links events to benchmarks</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#64748b;">refresh_token</div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_refresh_token</div>
|
|
<div><span class="fk">FK</span> id_user, id_program</div>
|
|
<div>token_hash, token_salt</div>
|
|
<div>expires_at, revoked_at</div>
|
|
<div>replaced_by_token_id</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity">
|
|
<div class="entity-header" style="background:#4f46e5;">report_prompt</div>
|
|
<div class="entity-fields">
|
|
<div><span class="pk">PK</span> id_report_prompt</div>
|
|
<div><span class="fk">FK</span> program_id</div>
|
|
<div>report_name</div>
|
|
<div>prompt_template (LLM)</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- relationship summary -->
|
|
<div style="margin-top:.9rem; background:white; border-radius:10px; padding:.85rem 1rem; border:1px solid #e5e7eb; font-size:.75rem; color:#374151;">
|
|
<strong>Key relationships:</strong>
|
|
school_district <strong>1→N</strong> program ·
|
|
program <strong>1→N</strong> student ·
|
|
student <strong>1→N</strong> goal (recursive parent/child) ·
|
|
goal <strong>1→N</strong> benchmark ·
|
|
goal <strong>1→N</strong> progress_event ·
|
|
progress_event <strong>M↔N</strong> benchmark (junction) ·
|
|
user <strong>M↔N</strong> program (via user_program + role) ·
|
|
user <strong>M↔N</strong> student (via user_student)
|
|
</div>
|
|
|
|
|
|
<!-- ══════════════════════════════════════════
|
|
4. ROLE HIERARCHY
|
|
══════════════════════════════════════════ -->
|
|
<div class="section-title"><span>👥</span> Role Hierarchy & Permissions</div>
|
|
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1rem; flex-wrap:wrap;" class="role-and-matrix">
|
|
|
|
<!-- pyramid -->
|
|
<div style="background:white; border-radius:12px; padding:1.2rem 1rem; box-shadow:0 2px 8px rgba(0,0,0,.07); border:1px solid #e5e7eb;">
|
|
<div style="font-size:.82rem; font-weight:700; margin-bottom:.9rem; color:#111827;">Role Tiers (highest → lowest)</div>
|
|
<div class="role-pyramid">
|
|
<div class="role-row">
|
|
<div class="role-chip" style="background:#111827; min-width:280px; text-align:center;">
|
|
super_admin · <span style="font-weight:400;font-size:.72rem;">full platform access</span>
|
|
</div>
|
|
</div>
|
|
<div class="role-row">
|
|
<div class="role-chip" style="background:#1d4ed8; min-width:240px; text-align:center;">
|
|
district_admin · <span style="font-weight:400;font-size:.72rem;">manages programs</span>
|
|
</div>
|
|
</div>
|
|
<div class="role-row">
|
|
<div class="role-chip" style="background:#2563eb; min-width:200px; text-align:center;">
|
|
program_admin · <span style="font-weight:400;font-size:.72rem;">manages users</span>
|
|
</div>
|
|
</div>
|
|
<div class="role-row">
|
|
<div class="role-chip" style="background:#16a34a; min-width:160px; text-align:center;">
|
|
teacher · <span style="font-weight:400;font-size:.72rem;">full student CRUD</span>
|
|
</div>
|
|
</div>
|
|
<div class="role-row">
|
|
<div class="role-chip" style="background:#d97706; min-width:120px; text-align:center;">
|
|
paraeducator · <span style="font-weight:400;font-size:.72rem;">log events</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- permission matrix -->
|
|
<div style="background:white; border-radius:12px; padding:1.2rem 1rem; box-shadow:0 2px 8px rgba(0,0,0,.07); border:1px solid #e5e7eb; overflow-x:auto;">
|
|
<div style="font-size:.82rem; font-weight:700; margin-bottom:.9rem; color:#111827;">Sample Permission Matrix</div>
|
|
<table class="matrix">
|
|
<thead>
|
|
<tr>
|
|
<th>Entity / Action</th>
|
|
<th>super_admin</th>
|
|
<th>district_admin</th>
|
|
<th>program_admin</th>
|
|
<th>teacher</th>
|
|
<th>paraeducator</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Student — Create</td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Student — Update</td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="mine">Mine</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Goal — Create</td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="mine">Mine</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Goal — Update</td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="mine">Mine</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>ProgressEvent — Create</td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="mine">Mine</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>ProgressEvent — Delete</td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="mine">Mine</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Program — Create</td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="mine">Mine</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>User — Create</td>
|
|
<td><span class="allow">Allow</span></td>
|
|
<td><span class="mine">Mine</span></td>
|
|
<td><span class="mine">Mine</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
<td><span class="deny">Deny</span></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div style="font-size:.65rem;color:#9ca3af;margin-top:.5rem;">
|
|
<span class="allow">Allow</span> = full access
|
|
<span class="mine">Mine</span> = own records only
|
|
<span class="deny">Deny</span> = not permitted
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<!-- ══════════════════════════════════════════
|
|
5. DEPLOYMENT
|
|
══════════════════════════════════════════ -->
|
|
<div class="section-title"><span>🐳</span> Deployment & Infrastructure</div>
|
|
|
|
<div class="info-cards">
|
|
|
|
<div class="info-card">
|
|
<h3>Docker Containers</h3>
|
|
<ul>
|
|
<li>mysql:8 (port 3309)</li>
|
|
<li>.NET API (port 8005)</li>
|
|
<li>Angular / Nginx (port 8006)</li>
|
|
<li>Traefik reverse proxy</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>Docker Networks</h3>
|
|
<ul>
|
|
<li>web — external HTTPS routing</li>
|
|
<li>backend — internal service mesh</li>
|
|
<li>API depends on MySQL healthcheck</li>
|
|
<li>UI depends on API (build time)</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>Environment Config</h3>
|
|
<ul>
|
|
<li>MYSQL_ROOT_PASSWORD</li>
|
|
<li>MYSQL_DATABASE / USER / PASSWORD</li>
|
|
<li>JWT_KEY (signing secret)</li>
|
|
<li>MYSQL_HOST / PORT</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>Frontend Environments</h3>
|
|
<ul>
|
|
<li>Dev: localhost:5000 (API)</li>
|
|
<li>Prod: winapi.opelly.me (API)</li>
|
|
<li>Served by Nginx in production</li>
|
|
<li>Angular CLI dev server locally</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>DB Initialisation</h3>
|
|
<ul>
|
|
<li>Schemas from db/docker-init/</li>
|
|
<li>51 stored procedures</li>
|
|
<li>Health check gates API startup</li>
|
|
<li>No ORM migrations — raw SQL</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>AI / External Services</h3>
|
|
<ul>
|
|
<li>Ollama LLM — llm.opelly.me</li>
|
|
<li>STT service — stt.opelly.me</li>
|
|
<li>Both called from API tier only</li>
|
|
<li>5-minute timeout on both</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<!-- ══════════════════════════════════════════
|
|
6. KEY DESIGN DECISIONS
|
|
══════════════════════════════════════════ -->
|
|
<div class="section-title"><span>⚙️</span> Key Design Decisions</div>
|
|
|
|
<div class="info-cards">
|
|
|
|
<div class="info-card">
|
|
<h3>Two-Phase JWT Auth</h3>
|
|
<ul>
|
|
<li>Phase 1: credentials → session token</li>
|
|
<li>Phase 2: program select → scoped JWT</li>
|
|
<li>Prevents data leakage before program chosen</li>
|
|
<li>JWT scoped to one program at a time</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>Refresh Token Rotation</h3>
|
|
<ul>
|
|
<li>New token issued on each refresh</li>
|
|
<li>Old token replaced (not deleted)</li>
|
|
<li>Genealogy tracked via replaced_by</li>
|
|
<li>Prevents replay attacks</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>Program-Scoped Multi-Tenancy</h3>
|
|
<ul>
|
|
<li>JWT contains program_id claim</li>
|
|
<li>All queries filtered by program</li>
|
|
<li>district_admin scoped to district</li>
|
|
<li>super_admin bypasses scope</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>Dapper + Stored Procedures</h3>
|
|
<ul>
|
|
<li>No ORM (Entity Framework)</li>
|
|
<li>Business logic close to data</li>
|
|
<li>Dapper maps rows to C# objects</li>
|
|
<li>51 procedures for full coverage</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>Angular Signals (not RxJS)</h3>
|
|
<ul>
|
|
<li>Auth state as reactive signals</li>
|
|
<li>Computed signals derive user context</li>
|
|
<li>Simpler than Observable chains</li>
|
|
<li>Auto-update dependent UI</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h3>Centralised Permission Matrix</h3>
|
|
<ul>
|
|
<li>Declarative rules per entity + action</li>
|
|
<li>Allow / MineOnly / Deny granularity</li>
|
|
<li>Evaluated at request time in API</li>
|
|
<li>Single source of truth for authz</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<footer>
|
|
WIN Student Goal Tracker — Architecture Overview | Generated 2026-04-15
|
|
</footer>
|
|
|
|
</body>
|
|
</html>
|