Latest docs

This commit is contained in:
ivan-pelly
2026-04-16 18:10:53 -07:00
parent 23db21e0bf
commit 91cc654cad
5 changed files with 3880 additions and 58 deletions
File diff suppressed because it is too large Load Diff
+953
View File
@@ -0,0 +1,953 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Database ERD — Student Progress Tracker</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0f1117;
--surface: #181a24;
--surface-hover: #1e2130;
--border: #2a2d3e;
--border-highlight: #3d4160;
--text: #c9cdd8;
--text-muted: #6b7084;
--text-bright: #eef0f6;
--pk: #f0b866;
--pk-bg: rgba(240, 184, 102, 0.08);
--fk: #6ea8fe;
--fk-bg: rgba(110, 168, 254, 0.08);
--col: #8b91a8;
--type: #5a5f75;
--line-color: #3d5a80;
--line-highlight: #6ea8fe;
--accent-green: #66d9a0;
--accent-red: #f07178;
--accent-purple: #c792ea;
--shadow: 0 4px 24px rgba(0,0,0,0.4);
--shadow-lg: 0 8px 40px rgba(0,0,0,0.6);
--radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'DM Sans', sans-serif;
overflow: hidden;
height: 100vh;
width: 100vw;
}
#toolbar {
position: fixed;
top: 0; left: 0; right: 0;
height: 52px;
background: var(--surface);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 20px;
z-index: 1000;
gap: 16px;
}
#toolbar h1 {
font-family: 'IBM Plex Mono', monospace;
font-size: 14px;
font-weight: 600;
color: var(--text-bright);
letter-spacing: -0.02em;
}
#toolbar .separator {
width: 1px;
height: 24px;
background: var(--border);
}
#toolbar .stat {
font-family: 'IBM Plex Mono', monospace;
font-size: 11px;
color: var(--text-muted);
}
#toolbar .stat span { color: var(--text); font-weight: 600; }
.toolbar-actions {
margin-left: auto;
display: flex;
gap: 8px;
}
.toolbar-btn {
background: var(--border);
border: none;
color: var(--text);
font-family: 'DM Sans', sans-serif;
font-size: 12px;
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
}
.toolbar-btn:hover { background: var(--border-highlight); color: var(--text-bright); }
#canvas-container {
position: absolute;
top: 52px; left: 0; right: 0; bottom: 0;
overflow: hidden;
cursor: grab;
}
#canvas-container:active { cursor: grabbing; }
#canvas {
position: absolute;
transform-origin: 0 0;
}
svg#lines {
position: absolute;
top: 0; left: 0;
pointer-events: none;
overflow: visible;
}
.table-node {
position: absolute;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
min-width: 240px;
box-shadow: var(--shadow);
cursor: move;
user-select: none;
transition: box-shadow 0.2s, border-color 0.2s;
}
.table-node:hover {
border-color: var(--border-highlight);
box-shadow: var(--shadow-lg);
z-index: 10;
}
.table-node.dragging {
z-index: 100;
border-color: var(--fk);
box-shadow: 0 0 0 1px var(--fk), var(--shadow-lg);
}
.table-header {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 8px;
}
.table-icon {
width: 20px; height: 20px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
flex-shrink: 0;
}
.table-header .table-name {
font-family: 'IBM Plex Mono', monospace;
font-size: 13px;
font-weight: 600;
color: var(--text-bright);
letter-spacing: -0.01em;
}
.table-body { padding: 4px 0; }
.column-row {
display: flex;
align-items: center;
padding: 3px 14px;
gap: 8px;
font-family: 'IBM Plex Mono', monospace;
font-size: 11.5px;
line-height: 1.6;
transition: background 0.1s;
}
.column-row:hover { background: var(--surface-hover); }
.col-badge {
font-size: 8px;
font-weight: 700;
padding: 1px 4px;
border-radius: 3px;
letter-spacing: 0.05em;
min-width: 20px;
text-align: center;
flex-shrink: 0;
}
.col-badge.pk { background: var(--pk-bg); color: var(--pk); }
.col-badge.fk { background: var(--fk-bg); color: var(--fk); }
.col-badge.empty { min-width: 20px; }
.col-name { color: var(--col); flex: 1; }
.col-name.pk-name { color: var(--pk); }
.col-name.fk-name { color: var(--fk); }
.col-type { color: var(--type); font-size: 10px; text-align: right; }
/* Legend */
#legend {
position: fixed;
bottom: 16px; left: 16px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 16px;
z-index: 1000;
font-size: 11px;
display: flex;
gap: 16px;
}
.legend-item { display: flex; align-items: center; gap: 6px; color: var(--text-muted); }
.legend-dot { width: 8px; height: 8px; border-radius: 2px; }
/* Zoom controls */
#zoom-controls {
position: fixed;
bottom: 16px; right: 16px;
display: flex;
gap: 4px;
z-index: 1000;
}
#zoom-controls button {
width: 36px; height: 36px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
#zoom-controls button:hover { background: var(--surface-hover); border-color: var(--border-highlight); }
/* Minimap */
#minimap {
position: fixed;
bottom: 60px; right: 16px;
width: 180px; height: 120px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
z-index: 1000;
overflow: hidden;
}
#minimap canvas { width: 100%; height: 100%; }
/* Relationship line styles */
.rel-line {
fill: none;
stroke: var(--line-color);
stroke-width: 1.5;
opacity: 0.5;
transition: opacity 0.2s, stroke 0.2s;
}
.rel-line.highlighted {
stroke: var(--line-highlight);
opacity: 1;
stroke-width: 2;
}
.rel-marker {
fill: var(--line-color);
transition: fill 0.2s;
}
.rel-marker.highlighted { fill: var(--line-highlight); }
.rel-label {
font-family: 'IBM Plex Mono', monospace;
font-size: 9px;
fill: var(--text-muted);
pointer-events: none;
}
</style>
</head>
<body>
<div id="toolbar">
<h1>⬡ ERD</h1>
<div class="separator"></div>
<div class="stat">Tables <span id="table-count">17</span></div>
<div class="separator"></div>
<div class="stat">Relations <span id="rel-count">0</span></div>
<div class="toolbar-actions">
<button class="toolbar-btn" onclick="resetView()">Reset View</button>
<button class="toolbar-btn" onclick="toggleLabels()">Toggle Labels</button>
<a href="technical.html" class="toolbar-btn" style="text-decoration:none;">← Technical Docs</a>
</div>
</div>
<div id="canvas-container">
<div id="canvas">
<svg id="lines" width="6000" height="4000"></svg>
</div>
</div>
<div id="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--pk)"></div> Primary Key</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--fk)"></div> Foreign Key</div>
<div class="legend-item">
<svg width="30" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="var(--line-color)" stroke-width="1.5"/></svg>
Relationship
</div>
</div>
<div id="zoom-controls">
<button onclick="zoomIn()">+</button>
<button onclick="zoomOut()"></button>
<button onclick="resetView()" style="font-size:12px"></button>
</div>
<div id="minimap"><canvas id="minimap-canvas"></canvas></div>
<script>
// ─── Schema Data ───
const schema = {
tables: {
user: {
columns: [
{ name: 'id_user', type: 'char(36)', pk: true },
{ name: 'email', type: 'varchar(255)' },
{ name: 'name', type: 'varchar(255)' },
{ name: 'password_hash', type: 'varchar(255)' },
{ name: 'password_salt', type: 'varchar(255)' },
{ name: 'password_updated_at', type: 'timestamp' },
{ name: 'failed_login_attempts', type: 'int' },
{ name: 'locked_until', type: 'timestamp' },
{ name: 'created_at', type: 'timestamp' }
],
color: '#6ea8fe'
},
student: {
columns: [
{ name: 'id_student', type: 'char(36)', pk: true },
{ name: 'id_program', type: 'char(36)', fk: 'program' },
{ name: 'identifier', type: 'varchar(50)' },
{ name: 'program_year', type: 'int' },
{ name: 'enrollment_date', type: 'date' },
{ name: 'next_iep_date', type: 'date' },
{ name: 'created_at', type: 'timestamp' }
],
color: '#66d9a0'
},
goal: {
columns: [
{ name: 'id_goal', type: 'char(36)', pk: true },
{ name: 'id_goal_parent', type: 'char(36)', fk: 'goal' },
{ name: 'id_student', type: 'char(36)', fk: 'student' },
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
{ name: 'description', type: 'text' },
{ name: 'category', type: 'varchar(255)' },
{ name: 'baseline', type: 'text' },
{ name: 'target_completion_date', type: 'date' },
{ name: 'close_date', type: 'date' },
{ name: 'achieved', type: 'tinyint(1)' },
{ name: 'close_notes', type: 'text' },
{ name: 'created_at', type: 'timestamp' },
{ name: 'updated_at', type: 'timestamp' }
],
color: '#f0b866'
},
benchmark: {
columns: [
{ name: 'id_benchmark', type: 'char(36)', pk: true },
{ name: 'id_goal', type: 'char(36)', fk: 'goal' },
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
{ name: 'benchmark', type: 'text' },
{ name: 'short_name', type: 'varchar(50)' },
{ name: 'created_at', type: 'datetime' },
{ name: 'updated_at', type: 'datetime' }
],
color: '#f0b866'
},
progress_event: {
columns: [
{ name: 'id_progress_event', type: 'char(36)', pk: true },
{ name: 'id_goal', type: 'char(36)', fk: 'goal' },
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
{ name: 'content', type: 'text' },
{ name: 'is_sensitive', type: 'tinyint(1)' },
{ name: 'created_at', type: 'timestamp' },
{ name: 'updated_at', type: 'timestamp' }
],
color: '#c792ea'
},
progress_event_benchmark: {
columns: [
{ name: 'id_progress_event_benchmark', type: 'char(36)', pk: true },
{ name: 'id_progress_event', type: 'char(36)', fk: 'progress_event' },
{ name: 'id_benchmark', type: 'char(36)', fk: 'benchmark' },
{ name: 'created_at', type: 'datetime' }
],
color: '#c792ea'
},
progress_report: {
columns: [
{ name: 'id_progress_report', type: 'char(36)', pk: true },
{ name: 'id_student', type: 'char(36)', fk: 'student' },
{ name: 'id_goal', type: 'char(36)', fk: 'goal' },
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
{ name: 'period', type: 'varchar(10)' },
{ name: 'year', type: 'int' },
{ name: 'summary', type: 'text' },
{ name: 'generated_at', type: 'timestamp' }
],
color: '#c792ea'
},
health_note: {
columns: [
{ name: 'id_health_note', type: 'char(36)', pk: true },
{ name: 'id_student', type: 'char(36)', fk: 'student' },
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
{ name: 'content', type: 'text' },
{ name: 'created_at', type: 'timestamp' }
],
color: '#66d9a0'
},
program: {
columns: [
{ name: 'id_program', type: 'char(36)', pk: true },
{ name: 'id_school_district', type: 'char(36)', fk: 'school_district' },
{ name: 'name', type: 'varchar(255)' },
{ name: 'description', type: 'text' },
{ name: 'created_at', type: 'timestamp' }
],
color: '#f07178'
},
school_district: {
columns: [
{ name: 'id_school_district', type: 'char(36)', pk: true },
{ name: 'name', type: 'varchar(255)' },
{ name: 'contact_email', type: 'varchar(255)' },
{ name: 'created_at', type: 'timestamp' }
],
color: '#f07178'
},
role: {
columns: [
{ name: 'id_role', type: 'char(36)', pk: true },
{ name: 'name', type: 'varchar(100)' },
{ name: 'internal_name', type: 'varchar(100)' },
{ name: 'description', type: 'text' },
{ name: 'created_at', type: 'timestamp' }
],
color: '#6ea8fe'
},
user_program: {
columns: [
{ name: 'id_user_program', type: 'char(36)', pk: true },
{ name: 'id_user', type: 'char(36)', fk: 'user' },
{ name: 'id_program', type: 'char(36)', fk: 'program' },
{ name: 'id_role', type: 'char(36)', fk: 'role' },
{ name: 'is_primary', type: 'tinyint(1)' },
{ name: 'status', type: 'varchar(20)' },
{ name: 'joined_at', type: 'timestamp' }
],
color: '#6ea8fe'
},
user_student: {
columns: [
{ name: 'id_user_student', type: 'char(36)', pk: true },
{ name: 'id_user', type: 'char(36)', fk: 'user' },
{ name: 'id_student', type: 'char(36)', fk: 'student' },
{ name: 'is_primary', type: 'tinyint(1)' }
],
color: '#6ea8fe'
},
password_history: {
columns: [
{ name: 'id_password_history', type: 'char(36)', pk: true },
{ name: 'id_user', type: 'char(36)', fk: 'user' },
{ name: 'password_hash', type: 'varchar(255)' },
{ name: 'created_at', type: 'timestamp' }
],
color: '#5a5f75'
},
password_reset_token: {
columns: [
{ name: 'id_password_reset_token', type: 'char(36)', pk: true },
{ name: 'id_user', type: 'char(36)', fk: 'user' },
{ name: 'token_hash', type: 'varchar(255)' },
{ name: 'expires_at', type: 'timestamp' },
{ name: 'created_at', type: 'timestamp' },
{ name: 'used_at', type: 'timestamp' },
{ name: 'invalidated_at', type: 'timestamp' },
{ name: 'request_ip', type: 'varchar(45)' }
],
color: '#5a5f75'
},
refresh_token: {
columns: [
{ name: 'id_refresh_token', type: 'char(36)', pk: true },
{ name: 'id_user', type: 'char(36)', fk: 'user' },
{ name: 'id_program', type: 'char(36)', fk: 'program' },
{ name: 'token_hash', type: 'varchar(512)' },
{ name: 'token_salt', type: 'varchar(512)' },
{ name: 'expires_at', type: 'timestamp' },
{ name: 'last_used_at', type: 'timestamp' },
{ name: 'revoked_at', type: 'timestamp' },
{ name: 'device_info', type: 'varchar(255)' },
{ name: 'user_agent', type: 'varchar(512)' },
{ name: 'replaced_by_token_id', type: 'char(36)', fk: 'refresh_token' },
{ name: 'created_at', type: 'timestamp' },
{ name: 'updated_at', type: 'timestamp' }
],
color: '#5a5f75'
},
ReportPrompt: {
columns: [
{ name: 'id_ReportPrompt', type: 'char(36)', pk: true },
{ name: 'prompt', type: 'text' },
{ name: 'reportname', type: 'char(100)' },
{ name: 'id_program', type: 'char(36)', fk: 'program' }
],
color: '#c792ea'
}
}
};
// Explicit foreign key relationships from the SQL constraints
const relationships = [
{ from: 'goal', fromCol: 'id_goal_parent', to: 'goal', toCol: 'id_goal', label: 'parent' },
{ from: 'goal', fromCol: 'id_student', to: 'student', toCol: 'id_student' },
{ from: 'goal', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
{ from: 'health_note', fromCol: 'id_student', to: 'student', toCol: 'id_student' },
{ from: 'health_note', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
{ from: 'password_history', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
{ from: 'password_reset_token', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
{ from: 'program', fromCol: 'id_school_district', to: 'school_district', toCol: 'id_school_district' },
{ from: 'progress_event', fromCol: 'id_goal', to: 'goal', toCol: 'id_goal' },
{ from: 'progress_event', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
{ from: 'progress_event_benchmark', fromCol: 'id_progress_event', to: 'progress_event', toCol: 'id_progress_event' },
{ from: 'progress_event_benchmark', fromCol: 'id_benchmark', to: 'benchmark', toCol: 'id_benchmark' },
{ from: 'progress_report', fromCol: 'id_student', to: 'student', toCol: 'id_student' },
{ from: 'progress_report', fromCol: 'id_goal', to: 'goal', toCol: 'id_goal' },
{ from: 'progress_report', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
{ from: 'refresh_token', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
{ from: 'refresh_token', fromCol: 'id_program', to: 'program', toCol: 'id_program' },
{ from: 'refresh_token', fromCol: 'replaced_by_token_id', to: 'refresh_token', toCol: 'id_refresh_token', label: 'replaces' },
{ from: 'benchmark', fromCol: 'id_goal', to: 'goal', toCol: 'id_goal' },
{ from: 'benchmark', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
{ from: 'student', fromCol: 'id_program', to: 'program', toCol: 'id_program' },
{ from: 'user_program', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
{ from: 'user_program', fromCol: 'id_program', to: 'program', toCol: 'id_program' },
{ from: 'user_program', fromCol: 'id_role', to: 'role', toCol: 'id_role' },
{ from: 'user_student', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
{ from: 'user_student', fromCol: 'id_student', to: 'student', toCol: 'id_student' },
{ from: 'ReportPrompt', fromCol: 'id_program', to: 'program', toCol: 'id_program' }
];
document.getElementById('rel-count').textContent = relationships.length;
// ─── Layout positions (hand-tuned for clarity) ───
const positions = {
// Core cluster
user: { x: 100, y: 300 },
role: { x: 100, y: 50 },
user_program: { x: 380, y: 80 },
user_student: { x: 380, y: 340 },
// Org cluster
school_district: { x: 680, y: 50 },
program: { x: 680, y: 260 },
student: { x: 680, y: 500 },
// Goals & progress cluster
goal: { x: 1050, y: 260 },
benchmark: { x: 1050, y: 50 },
progress_event: { x: 1380, y: 100 },
progress_event_benchmark: { x: 1380, y: 340 },
progress_report: { x: 1050, y: 570 },
health_note: { x: 680, y: 760 },
ReportPrompt: { x: 1380, y: 570 },
// Auth cluster
password_history: { x: 100, y: 620 },
password_reset_token: { x: 100, y: 800 },
refresh_token: { x: 380, y: 620 }
};
// ─── Rendering ───
const canvas = document.getElementById('canvas');
const svgLines = document.getElementById('lines');
const container = document.getElementById('canvas-container');
const tableElements = {};
let scale = 0.85;
let panX = 50, panY = 20;
let showLabels = true;
function renderTables() {
for (const [tableName, table] of Object.entries(schema.tables)) {
const pos = positions[tableName] || { x: 0, y: 0 };
const el = document.createElement('div');
el.className = 'table-node';
el.id = `table-${tableName}`;
el.style.left = pos.x + 'px';
el.style.top = pos.y + 'px';
const headerColor = table.color || '#6ea8fe';
let html = `
<div class="table-header">
<div class="table-icon" style="background:${headerColor}22;color:${headerColor}">⬡</div>
<span class="table-name">${tableName}</span>
</div>
<div class="table-body">
`;
for (const col of table.columns) {
const isPk = col.pk;
const isFk = col.fk;
let badge = '<span class="col-badge empty"></span>';
if (isPk) badge = '<span class="col-badge pk">PK</span>';
else if (isFk) badge = '<span class="col-badge fk">FK</span>';
let nameClass = 'col-name';
if (isPk) nameClass += ' pk-name';
else if (isFk) nameClass += ' fk-name';
html += `
<div class="column-row" data-col="${col.name}">
${badge}
<span class="${nameClass}">${col.name}</span>
<span class="col-type">${col.type}</span>
</div>`;
}
html += '</div>';
el.innerHTML = html;
canvas.appendChild(el);
tableElements[tableName] = el;
makeDraggable(el, tableName);
}
}
function getColumnY(tableName, colName) {
const el = tableElements[tableName];
if (!el) return 0;
const row = el.querySelector(`[data-col="${colName}"]`);
if (!row) return el.offsetTop + 20;
return el.offsetTop + row.offsetTop + row.offsetHeight / 2;
}
function getTableCenter(tableName) {
const el = tableElements[tableName];
if (!el) return { x: 0, y: 0 };
return {
x: el.offsetLeft + el.offsetWidth / 2,
y: el.offsetTop + el.offsetHeight / 2
};
}
function drawRelationships() {
svgLines.innerHTML = '';
// Arrow marker defs
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
marker.setAttribute('id', 'arrowhead');
marker.setAttribute('markerWidth', '8');
marker.setAttribute('markerHeight', '6');
marker.setAttribute('refX', '8');
marker.setAttribute('refY', '3');
marker.setAttribute('orient', 'auto');
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
poly.setAttribute('points', '0 0, 8 3, 0 6');
poly.setAttribute('class', 'rel-marker');
marker.appendChild(poly);
defs.appendChild(marker);
const markerH = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
markerH.setAttribute('id', 'arrowhead-hl');
markerH.setAttribute('markerWidth', '8');
markerH.setAttribute('markerHeight', '6');
markerH.setAttribute('refX', '8');
markerH.setAttribute('refY', '3');
markerH.setAttribute('orient', 'auto');
const polyH = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
polyH.setAttribute('points', '0 0, 8 3, 0 6');
polyH.setAttribute('class', 'rel-marker highlighted');
markerH.appendChild(polyH);
defs.appendChild(markerH);
svgLines.appendChild(defs);
for (const rel of relationships) {
const fromEl = tableElements[rel.from];
const toEl = tableElements[rel.to];
if (!fromEl || !toEl) continue;
const fromY = getColumnY(rel.from, rel.fromCol);
const toY = getColumnY(rel.to, rel.toCol);
const fromLeft = fromEl.offsetLeft;
const fromRight = fromEl.offsetLeft + fromEl.offsetWidth;
const toLeft = toEl.offsetLeft;
const toRight = toEl.offsetLeft + toEl.offsetWidth;
let x1, x2;
// Self-referencing
if (rel.from === rel.to) {
x1 = fromRight;
x2 = fromRight;
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const loopSize = 40;
const d = `M ${x1} ${fromY} C ${x1 + loopSize} ${fromY - loopSize}, ${x2 + loopSize} ${toY + loopSize}, ${x2} ${toY}`;
path.setAttribute('d', d);
path.setAttribute('class', 'rel-line');
path.setAttribute('marker-end', 'url(#arrowhead)');
path.dataset.from = rel.from;
path.dataset.to = rel.to;
svgLines.appendChild(path);
if (showLabels && rel.label) {
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x1 + loopSize + 4);
text.setAttribute('y', (fromY + toY) / 2);
text.setAttribute('class', 'rel-label');
text.textContent = rel.label;
svgLines.appendChild(text);
}
continue;
}
// Determine which side to connect
const fromCX = (fromLeft + fromRight) / 2;
const toCX = (toLeft + toRight) / 2;
if (fromCX < toCX) {
x1 = fromRight;
x2 = toLeft;
} else {
x1 = fromLeft;
x2 = toRight;
}
const dx = Math.abs(x2 - x1);
const cpOffset = Math.max(40, dx * 0.35);
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const cp1x = x1 < x2 ? x1 + cpOffset : x1 - cpOffset;
const cp2x = x1 < x2 ? x2 - cpOffset : x2 + cpOffset;
const d = `M ${x1} ${fromY} C ${cp1x} ${fromY}, ${cp2x} ${toY}, ${x2} ${toY}`;
path.setAttribute('d', d);
path.setAttribute('class', 'rel-line');
path.setAttribute('marker-end', 'url(#arrowhead)');
path.dataset.from = rel.from;
path.dataset.to = rel.to;
svgLines.appendChild(path);
if (showLabels && rel.label) {
const mx = (x1 + x2) / 2;
const my = (fromY + toY) / 2 - 6;
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', mx);
text.setAttribute('y', my);
text.setAttribute('class', 'rel-label');
text.setAttribute('text-anchor', 'middle');
text.textContent = rel.label;
svgLines.appendChild(text);
}
}
}
// ─── Interaction: Drag ───
function makeDraggable(el, tableName) {
let startX, startY, origX, origY;
el.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
e.stopPropagation();
el.classList.add('dragging');
startX = e.clientX;
startY = e.clientY;
origX = el.offsetLeft;
origY = el.offsetTop;
function onMove(e) {
const dx = (e.clientX - startX) / scale;
const dy = (e.clientY - startY) / scale;
el.style.left = (origX + dx) + 'px';
el.style.top = (origY + dy) + 'px';
positions[tableName].x = origX + dx;
positions[tableName].y = origY + dy;
drawRelationships();
updateMinimap();
}
function onUp() {
el.classList.remove('dragging');
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
// Hover highlighting
el.addEventListener('mouseenter', () => highlightTable(tableName));
el.addEventListener('mouseleave', () => clearHighlights());
}
function highlightTable(tableName) {
const lines = svgLines.querySelectorAll('.rel-line');
lines.forEach(line => {
if (line.dataset.from === tableName || line.dataset.to === tableName) {
line.classList.add('highlighted');
line.setAttribute('marker-end', 'url(#arrowhead-hl)');
} else {
line.style.opacity = '0.15';
}
});
}
function clearHighlights() {
const lines = svgLines.querySelectorAll('.rel-line');
lines.forEach(line => {
line.classList.remove('highlighted');
line.setAttribute('marker-end', 'url(#arrowhead)');
line.style.opacity = '';
});
}
// ─── Pan & Zoom ───
let isPanning = false, panStartX, panStartY;
container.addEventListener('mousedown', (e) => {
if (e.target !== container && e.target !== canvas && e.target.tagName !== 'svg') return;
isPanning = true;
panStartX = e.clientX - panX;
panStartY = e.clientY - panY;
container.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isPanning) return;
panX = e.clientX - panStartX;
panY = e.clientY - panStartY;
applyTransform();
updateMinimap();
});
document.addEventListener('mouseup', () => {
isPanning = false;
container.style.cursor = 'grab';
});
container.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.92 : 1.08;
const newScale = Math.max(0.2, Math.min(2, scale * delta));
// Zoom toward cursor
const rect = container.getBoundingClientRect();
const cx = e.clientX - rect.left;
const cy = e.clientY - rect.top;
panX = cx - (cx - panX) * (newScale / scale);
panY = cy - (cy - panY) * (newScale / scale);
scale = newScale;
applyTransform();
updateMinimap();
}, { passive: false });
function applyTransform() {
canvas.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
}
function zoomIn() { scale = Math.min(2, scale * 1.2); applyTransform(); updateMinimap(); }
function zoomOut() { scale = Math.max(0.2, scale * 0.8); applyTransform(); updateMinimap(); }
function resetView() {
scale = 0.85;
panX = 50;
panY = 20;
applyTransform();
updateMinimap();
}
function toggleLabels() {
showLabels = !showLabels;
drawRelationships();
}
// ─── Minimap ───
const minimapCanvas = document.getElementById('minimap-canvas');
const mCtx = minimapCanvas.getContext('2d');
function updateMinimap() {
const mw = 180, mh = 120;
minimapCanvas.width = mw * 2;
minimapCanvas.height = mh * 2;
minimapCanvas.style.width = mw + 'px';
minimapCanvas.style.height = mh + 'px';
mCtx.scale(2, 2);
mCtx.clearRect(0, 0, mw, mh);
mCtx.fillStyle = '#0f1117';
mCtx.fillRect(0, 0, mw, mh);
// Find bounds
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const [name, pos] of Object.entries(positions)) {
const el = tableElements[name];
if (!el) continue;
minX = Math.min(minX, pos.x);
minY = Math.min(minY, pos.y);
maxX = Math.max(maxX, pos.x + el.offsetWidth);
maxY = Math.max(maxY, pos.y + el.offsetHeight);
}
const padding = 40;
minX -= padding; minY -= padding;
maxX += padding; maxY += padding;
const scaleX = mw / (maxX - minX);
const scaleY = mh / (maxY - minY);
const ms = Math.min(scaleX, scaleY);
// Draw tables
for (const [name, pos] of Object.entries(positions)) {
const el = tableElements[name];
if (!el) continue;
const color = schema.tables[name]?.color || '#6ea8fe';
mCtx.fillStyle = color + '60';
mCtx.fillRect(
(pos.x - minX) * ms,
(pos.y - minY) * ms,
el.offsetWidth * ms,
el.offsetHeight * ms
);
}
// Draw viewport
const rect = container.getBoundingClientRect();
const vx = (-panX / scale - minX) * ms;
const vy = (-panY / scale - minY) * ms;
const vw = (rect.width / scale) * ms;
const vh = (rect.height / scale) * ms;
mCtx.strokeStyle = '#6ea8fe';
mCtx.lineWidth = 1;
mCtx.strokeRect(vx, vy, vw, vh);
}
// ─── Init ───
renderTables();
drawRelationships();
applyTransform();
setTimeout(updateMinimap, 100);
window.addEventListener('resize', updateMinimap);
</script>
</body>
</html>
+1
View File
@@ -354,6 +354,7 @@
<a href="ui.html" class="btn">UI Documentation</a> <a href="ui.html" class="btn">UI Documentation</a>
<a href="um.html" class="btn">User Manual Documentation</a> <a href="um.html" class="btn">User Manual Documentation</a>
<a href="technical.html" class="btn">Technical Documentation</a> <a href="technical.html" class="btn">Technical Documentation</a>
<a href="architecture.html" class="btn" style="background:#111827;">Architecture Overview</a>
<a href="p6video.html" class="btn" style="background:#7816e9;">🎬 P6 Presentation Video</a> <a href="p6video.html" class="btn" style="background:#7816e9;">🎬 P6 Presentation Video</a>
</div> </div>
+1511 -6
View File
File diff suppressed because it is too large Load Diff
+310 -52
View File
@@ -193,7 +193,6 @@
} }
.callout{ .callout{
border-left:4px solid var(--primary);
background:#eff6ff; background:#eff6ff;
padding:1rem; padding:1rem;
border-radius:12px; border-radius:12px;
@@ -201,10 +200,13 @@
} }
.callout.success{ .callout.success{
border-left-color:var(--success);
background:#f0fdf4; background:#f0fdf4;
} }
.callout.warn{
background:#fffbeb;
}
code{ code{
background:#eef2ff; background:#eef2ff;
padding:.15rem .4rem; padding:.15rem .4rem;
@@ -244,6 +246,29 @@
margin-bottom:.25rem; margin-bottom:.25rem;
} }
.role-table{
width:100%;
border-collapse:collapse;
margin-top:1rem;
font-size:.95rem;
}
.role-table th,
.role-table td{
text-align:left;
padding:.5rem .75rem;
border:1px solid var(--border);
}
.role-table th{
background:#f3f4f6;
font-weight:600;
}
.role-table tr:nth-child(even) td{
background:#f9fafb;
}
.footer{ .footer{
text-align:center; text-align:center;
color:var(--muted); color:var(--muted);
@@ -263,8 +288,8 @@
<header> <header>
<h1>WIN Student Goal Tracker</h1> <h1>WIN Student Goal Tracker</h1>
<p> <p>
User manual for teachers and program staff using the WIN platform to manage User manual for teachers, program staff, and administrators using the WIN
students, goals, benchmarks, and progress events. platform to manage students, goals, benchmarks, and progress events.
</p> </p>
<div class="hero-actions"> <div class="hero-actions">
@@ -288,24 +313,28 @@
<h2>Contents</h2> <h2>Contents</h2>
<ul> <ul>
<li><a href="#overview">1. Overview</a></li> <li><a href="#overview">1. Overview</a></li>
<li><a href="#access">2. Accessing the Application</a></li> <li><a href="#roles">2. User Roles</a></li>
<li><a href="#program">3. Selecting a Program</a></li> <li><a href="#registration">3. Registering a New District</a></li>
<li><a href="#dashboard">4. Student Dashboard</a></li> <li><a href="#access">4. Accessing the Application</a></li>
<li><a href="#add-student">5. Adding &amp; Editing Students</a></li> <li><a href="#program">5. Selecting a Program</a></li>
<li><a href="#goals">6. Managing Goals</a></li> <li><a href="#dashboard">6. Student Dashboard</a></li>
<li><a href="#benchmarks">7. Managing Benchmarks</a></li> <li><a href="#add-student">7. Adding &amp; Editing Students</a></li>
<li><a href="#events">8. Recording Progress Events</a></li> <li><a href="#goals">8. Managing Goals</a></li>
<li><a href="#deleting">9. Deleting Records</a></li> <li><a href="#benchmarks">9. Managing Benchmarks</a></li>
<li><a href="#reports">10. Reports</a></li> <li><a href="#events">10. Recording Progress Events</a></li>
<li><a href="#mobile">11. Mobile Experience</a></li> <li><a href="#deleting">11. Deleting Records</a></li>
<li><a href="#workflow">12. Typical Workflow</a></li> <li><a href="#reports">12. Reports</a></li>
<li><a href="#tips">13. Usage Notes</a></li> <li><a href="#admin">13. Administration</a></li>
<li><a href="#mobile">14. Mobile Experience</a></li>
<li><a href="#workflow">15. Typical Workflow</a></li>
<li><a href="#tips">16. Usage Notes</a></li>
</ul> </ul>
<p class="small"> <p class="small">
Demo login:<br /> Prototype demo login:<br />
<code>opelly@gmail.com</code><br /> <code>opelly@gmail.com</code><br />
<code>1234</code> <code>1234</code><br />
(Teacher role — for demo use only)
</p> </p>
</aside> </aside>
@@ -340,14 +369,128 @@
</div> </div>
<div class="callout"> <div class="callout">
This manual covers the current application workflow as implemented This manual covers teacher and program staff workflows as well as
in the deployed prototype. Features relating to district registration district administration features including program management and
and administration are documented separately. user provisioning.
</div>
</section>
<section id="roles" class="card">
<h2 class="section-title">2. User Roles</h2>
<p class="muted">
Every user account is assigned a role when added to a program. The
role determines which actions that user can perform. There are five
roles in the system.
</p>
<table class="role-table">
<thead>
<tr>
<th>Role</th>
<th>Description</th>
<th>Key Permissions</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Teacher</strong></td>
<td>Standard instructional staff member</td>
<td>
Full access to their own students — create, edit, and delete
students, goals, benchmarks, and progress events. Can run
reports and use AI suggestions.
</td>
</tr>
<tr>
<td><strong>Paraeducator</strong></td>
<td>Support staff assisting teachers</td>
<td>
Can view students and log progress events. Cannot create or
delete students, goals, or benchmarks, and cannot run reports
or use AI suggestions.
</td>
</tr>
<tr>
<td><strong>Program Admin</strong></td>
<td>Staff member who manages an entire program</td>
<td>
Same as Teacher but scoped across all students in the program,
not just their own. Can run reports.
</td>
</tr>
<tr>
<td><strong>District Admin</strong></td>
<td>Administrator who manages a school district</td>
<td>
All Program Admin abilities plus access to the Administration
panel. Can create and edit programs and add users to any
program within the district.
</td>
</tr>
<tr>
<td><strong>Super Admin</strong></td>
<td>System-level administrator</td>
<td>
Full access across all districts and programs. Can assign any
role, including Super Admin. Reserved for platform operators.
</td>
</tr>
</tbody>
</table>
<div class="callout">
A user's role is set when their account is created by a District
Admin. If you are unsure of your role, contact your district
administrator.
</div>
</section>
<section id="registration" class="card">
<h2 class="section-title">3. Registering a New District</h2>
<p class="muted">
If your school district is not yet set up in the system, a district
administrator can self-register to create the district account, an
initial program, and their own login in one step.
</p>
<div class="callout warn">
Registration creates a new, independent district. If your district
already exists, do not register again — contact your existing
district administrator to have a user account created for you.
</div>
<h3>How to register</h3>
<ol>
<li>
Go to <code>https://win.opelly.me/register</code> or click
<strong>Register</strong> from the login page.
</li>
<li>
Fill in <strong>Your Details</strong>: name, email address, and
password. This becomes your login.
</li>
<li>
Fill in <strong>District Details</strong>: district name and an
optional contact email for the district.
</li>
<li>
Fill in <strong>Your First Program</strong>: the name and optional
description of the first program in your district (e.g.
"Special Education").
</li>
<li>Click <strong>Create District &amp; Account</strong>.</li>
</ol>
<div class="callout success">
After registration you will be directed to the login page. Sign in
with your new credentials. You will be assigned the District Admin
role automatically and can add more programs and users from the
Administration panel.
</div> </div>
</section> </section>
<section id="access" class="card"> <section id="access" class="card">
<h2 class="section-title">2. Accessing the Application</h2> <h2 class="section-title">4. Accessing the Application</h2>
<p> <p>
To begin, open the login page at To begin, open the login page at
<code>https://win.opelly.me/login</code>. <code>https://win.opelly.me/login</code>.
@@ -365,15 +508,21 @@
After a successful login, the system will take you to the program After a successful login, the system will take you to the program
selection screen. selection screen.
</div> </div>
<div class="callout">
If your account is locked after repeated failed attempts, you will
see a message indicating the account is temporarily unavailable.
Wait a few minutes before trying again or contact your administrator.
</div>
</section> </section>
<section id="program" class="card"> <section id="program" class="card">
<h2 class="section-title">3. Selecting a Program</h2> <h2 class="section-title">5. Selecting a Program</h2>
<p class="muted"> <p class="muted">
After authentication, the system displays the list of programs After authentication, the system displays the list of programs
available to the logged-in user. Each program card shows the available to the logged-in user. Each program card shows the
program name, your role (e.g. Teacher, Admin), and whether it is program name, your role (e.g. Teacher, District Admin), and whether
your primary program. it is your primary program.
</p> </p>
<ol> <ol>
@@ -381,10 +530,16 @@
<li>Click the program you wish to work in.</li> <li>Click the program you wish to work in.</li>
<li>The application will open the main dashboard for that program.</li> <li>The application will open the main dashboard for that program.</li>
</ol> </ol>
<div class="callout">
A user can belong to multiple programs with different roles in each.
Selecting a program loads a session scoped to that program — switch
programs by logging out and logging back in.
</div>
</section> </section>
<section id="dashboard" class="card"> <section id="dashboard" class="card">
<h2 class="section-title">4. Student Dashboard</h2> <h2 class="section-title">6. Student Dashboard</h2>
<p> <p>
The main dashboard displays the students assigned to the current The main dashboard displays the students assigned to the current
teacher or user. teacher or user.
@@ -403,6 +558,10 @@
between viewing only your own students and all students in between viewing only your own students and all students in
the program. the program.
</li> </li>
<li>
Students may be organized into labeled groups if a Program
Admin or District Admin has configured groupings.
</li>
<li>Click a student to open their goal workspace.</li> <li>Click a student to open their goal workspace.</li>
</ul> </ul>
</div> </div>
@@ -410,9 +569,14 @@
<h3>Navigation</h3> <h3>Navigation</h3>
<ul class="info-list"> <ul class="info-list">
<li> <li>
The sidebar also provides links to <strong>Reports</strong> The sidebar provides links to <strong>Reports</strong>
and <strong>Log Out</strong>. and <strong>Log Out</strong>.
</li> </li>
<li>
District Admins and Super Admins also see an
<strong>Admin</strong> link that opens the Administration
panel (see <a href="#admin">Section 13</a>).
</li>
<li>Click the edit icon next to a student to update their name or IEP date.</li> <li>Click the edit icon next to a student to update their name or IEP date.</li>
</ul> </ul>
</div> </div>
@@ -420,10 +584,11 @@
</section> </section>
<section id="add-student" class="card"> <section id="add-student" class="card">
<h2 class="section-title">5. Adding &amp; Editing Students</h2> <h2 class="section-title">7. Adding &amp; Editing Students</h2>
<p class="muted"> <p class="muted">
Teachers can add a new student record directly from the dashboard, Teachers and Program Admins can add a new student record directly
or edit an existing student's information. from the dashboard, or edit an existing student's information.
Paraeducators cannot add or edit students.
</p> </p>
<h3>Adding a student</h3> <h3>Adding a student</h3>
@@ -447,7 +612,7 @@
</section> </section>
<section id="goals" class="card"> <section id="goals" class="card">
<h2 class="section-title">6. Managing Goals</h2> <h2 class="section-title">8. Managing Goals</h2>
<p> <p>
Selecting a student opens the goal workspace for that student. Selecting a student opens the goal workspace for that student.
Goals are the main tracking objects used to measure student Goals are the main tracking objects used to measure student
@@ -509,13 +674,18 @@
</section> </section>
<section id="benchmarks" class="card"> <section id="benchmarks" class="card">
<h2 class="section-title">7. Managing Benchmarks</h2> <h2 class="section-title">9. Managing Benchmarks</h2>
<p> <p>
Benchmarks break a goal into smaller, measurable steps. This makes it Benchmarks break a goal into smaller, measurable steps. This makes it
easier to see incremental progress over time. Each benchmark has a easier to see incremental progress over time. Each benchmark has a
full description and an optional short name used for compact display. full description and an optional short name used for compact display.
</p> </p>
<p class="muted">
Teachers and Program Admins can add and edit benchmarks.
Paraeducators can view benchmarks but cannot create or modify them.
</p>
<h3>To add a benchmark</h3> <h3>To add a benchmark</h3>
<ol> <ol>
<li>Select the goal tab for the relevant goal.</li> <li>Select the goal tab for the relevant goal.</li>
@@ -530,7 +700,8 @@
When adding a new benchmark, you can click the When adding a new benchmark, you can click the
<strong>✦ Suggest with AI</strong> button. The system will analyze <strong>✦ Suggest with AI</strong> button. The system will analyze
the student's goal and generate a recommended benchmark that you the student's goal and generate a recommended benchmark that you
can accept, modify, or discard before saving. can accept, modify, or discard before saving. This feature is
available to Teachers and Program Admins only.
</p> </p>
<h3>Editing a benchmark</h3> <h3>Editing a benchmark</h3>
@@ -545,10 +716,11 @@
</section> </section>
<section id="events" class="card"> <section id="events" class="card">
<h2 class="section-title">8. Recording Progress Events</h2> <h2 class="section-title">10. Recording Progress Events</h2>
<p class="muted"> <p class="muted">
Progress events document meaningful activities, milestones, or notes Progress events document meaningful activities, milestones, or notes
related to a student's goal. related to a student's goal. All roles (including Paraeducators) can
log progress events.
</p> </p>
<h3>To log a progress event</h3> <h3>To log a progress event</h3>
@@ -595,9 +767,10 @@
</section> </section>
<section id="deleting" class="card"> <section id="deleting" class="card">
<h2 class="section-title">9. Deleting Records</h2> <h2 class="section-title">11. Deleting Records</h2>
<p class="muted"> <p class="muted">
Students, goals, benchmarks, and progress events can all be deleted. Students, goals, benchmarks, and progress events can all be deleted
by Teachers and Program Admins. Paraeducators cannot delete records.
Deletions are permanent and cannot be undone. Deletions are permanent and cannot be undone.
</p> </p>
@@ -632,11 +805,12 @@
</section> </section>
<section id="reports" class="card"> <section id="reports" class="card">
<h2 class="section-title">10. Reports</h2> <h2 class="section-title">12. Reports</h2>
<p class="muted"> <p class="muted">
The Reports section provides tools for extracting and summarizing The Reports section provides tools for extracting and summarizing
student progress data. Access it from the <strong>Reports</strong> student progress data. Access it from the <strong>Reports</strong>
link in the sidebar. link in the sidebar. Reports are available to Teachers and Program
Admins. Paraeducators do not have access to the Reports section.
</p> </p>
<h3>Student Progress Report</h3> <h3>Student Progress Report</h3>
@@ -665,8 +839,73 @@
</div> </div>
</section> </section>
<section id="admin" class="card">
<h2 class="section-title">13. Administration</h2>
<p class="muted">
The Administration panel is available to District Admins and Super
Admins. It is accessed via the <strong>Admin</strong> link in the
sidebar, which is only visible to users with those roles.
</p>
<p>
The Admin panel has two tabs: <strong>Programs</strong> and
<strong>Users</strong>.
</p>
<h3>Programs tab</h3>
<p>
Lists all programs in your district. District Admins can create new
programs and edit existing ones.
</p>
<h4>Adding a program</h4>
<ol>
<li>Click <strong>+ Add Program</strong>.</li>
<li>Enter a <strong>Program Name</strong> (required) and an optional <strong>Description</strong>.</li>
<li>Click <strong>Create</strong>.</li>
</ol>
<h4>Editing a program</h4>
<ol>
<li>Click <strong>Edit</strong> next to the program.</li>
<li>Update the name or description.</li>
<li>Click <strong>Save</strong>.</li>
</ol>
<h3>Users tab</h3>
<p>
Lists all users across all programs in your district, showing each
user's name, email, role, and assigned program.
</p>
<h4>Adding a user</h4>
<ol>
<li>Click <strong>+ Add User</strong>.</li>
<li>Enter the user's <strong>Name</strong>, <strong>Email</strong>, and a temporary <strong>Password</strong>.</li>
<li>Select the <strong>Program</strong> to assign the user to.</li>
<li>Select the user's <strong>Role</strong> (see <a href="#roles">Section 2</a> for role descriptions).</li>
<li>Click <strong>Create User</strong>.</li>
</ol>
<p>
The new user can immediately log in with the email and password you
provided. Share the credentials with them securely.
</p>
<div class="callout">
District Admins cannot assign the Super Admin role. Only a Super
Admin can elevate another user to Super Admin.
</div>
<div class="callout warn">
There is currently no in-app password reset. If a user forgets their
password, a District Admin will need to create a new account or
contact the platform operator for assistance.
</div>
</section>
<section id="mobile" class="card"> <section id="mobile" class="card">
<h2 class="section-title">11. Mobile Experience</h2> <h2 class="section-title">14. Mobile Experience</h2>
<p class="muted"> <p class="muted">
The application automatically detects whether you are using a The application automatically detects whether you are using a
touch-based mobile device and presents a touch-optimized interface. touch-based mobile device and presents a touch-optimized interface.
@@ -691,37 +930,56 @@
<div class="callout"> <div class="callout">
Some management features — such as adding students, creating goals, Some management features — such as adding students, creating goals,
editing benchmarks, running reports, and deleting records — are editing benchmarks, running reports, accessing the Admin panel, and
only available in the desktop interface. Use a desktop browser for deleting records — are only available in the desktop interface. Use
full administrative access. a desktop browser for full administrative access.
</div> </div>
</section> </section>
<section id="workflow" class="card"> <section id="workflow" class="card">
<h2 class="section-title">12. Typical Workflow</h2> <h2 class="section-title">15. Typical Workflow</h2>
<h3>For a Teacher or Program Admin</h3>
<ol> <ol>
<li>Log into the application.</li> <li>Log into the application and select your program.</li>
<li>Select the appropriate program.</li> <li>Review the list of assigned students in the sidebar.</li>
<li>Review the list of assigned students.</li>
<li>Add a student if needed.</li> <li>Add a student if needed.</li>
<li>Open a student record.</li> <li>Open a student record and review or create goals.</li>
<li>Review or create goals.</li>
<li>Add benchmarks to define milestones (use AI suggestions when helpful).</li> <li>Add benchmarks to define milestones (use AI suggestions when helpful).</li>
<li>Record progress events as the student advances, linking relevant benchmarks.</li> <li>Record progress events as the student advances, linking relevant benchmarks.</li>
<li>Run reports as needed for IEP meetings or program reviews.</li> <li>Run reports as needed for IEP meetings or program reviews.</li>
</ol> </ol>
<h3>For a Paraeducator</h3>
<ol>
<li>Log into the application and select your program.</li>
<li>Browse students using the sidebar.</li>
<li>Open a student record and select a goal.</li>
<li>Switch to the <strong>Progress Events</strong> sub-tab and log a new event.</li>
</ol>
<h3>For a District Admin (initial setup)</h3>
<ol>
<li>Register at <code>/register</code> to create your district and first program.</li>
<li>Log in and open the <strong>Admin</strong> panel from the sidebar.</li>
<li>Create additional programs if needed.</li>
<li>Add teacher and paraeducator accounts under the Users tab.</li>
<li>Share login credentials with each user.</li>
</ol>
</section> </section>
<section id="tips" class="card"> <section id="tips" class="card">
<h2 class="section-title">13. Usage Notes</h2> <h2 class="section-title">16. Usage Notes</h2>
<ul> <ul>
<li>Keep student names consistent to avoid duplicate entries.</li> <li>Keep student names consistent (use initials or a non-identifying label) to avoid duplicate entries and to protect student privacy.</li>
<li>Update progress events regularly so the record stays current.</li> <li>Update progress events regularly so the record stays current.</li>
<li>Use benchmarks to make large goals easier to track.</li> <li>Use benchmarks to make large goals easier to track.</li>
<li>Link progress events to benchmarks for richer reporting.</li> <li>Link progress events to benchmarks for richer reporting.</li>
<li>Review student cards often to spot inactivity or missing updates.</li> <li>Review student cards often to spot inactivity or missing updates.</li>
<li>Use the mobile app for quick progress logging in the field.</li> <li>Use the mobile app for quick progress logging in the field.</li>
<li>Log out when finished using the system on a shared device.</li> <li>Log out when finished using the system on a shared device.</li>
<li>If your login is locked, wait a few minutes and try again, or contact your district administrator.</li>
<li>District Admins: share newly created passwords with users through a secure channel and advise them not to share their credentials.</li>
</ul> </ul>
<a href="index.html" class="btn btn-primary back-link">Back to Project Home</a> <a href="index.html" class="btn btn-primary back-link">Back to Project Home</a>
@@ -731,7 +989,7 @@
</main> </main>
<div class="footer"> <div class="footer">
© 2026 WIN Student Goal Tracker. &copy; 2026 WIN Student Goal Tracker.
</div> </div>
</body> </body>
</html> </html>