Adds a GET /api/spelling-tests endpoint that returns all standalone test results grouped by student. The Gradebook panel now shows a "Spelling Test History" section below the course gradebook table. Removed the "Test results" section from the Spelling panel left column. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
456 lines
23 KiB
HTML
456 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Parent Dashboard</title>
|
|
<link rel="stylesheet" href="styles.css">
|
|
</head>
|
|
<body>
|
|
|
|
<section id="pin-gate" class="view parent">
|
|
<h1>👩🏫 Parent Dashboard</h1>
|
|
<form id="pin-form">
|
|
<input id="pin-input" type="password" inputmode="numeric" placeholder="Enter PIN" aria-label="Parent PIN">
|
|
<button type="submit">Unlock</button>
|
|
</form>
|
|
<p id="pin-error" class="error" hidden>Wrong PIN, try again.</p>
|
|
<p class="hint">Default PIN is 1234 — change it in Settings after unlocking.</p>
|
|
<p><a class="link-btn" href="index.html">← Back to kid picker</a></p>
|
|
</section>
|
|
|
|
<div id="console" hidden>
|
|
<header class="admin-header">
|
|
<a href="index.html" class="admin-logo">🎓 Homeschool</a>
|
|
<nav class="admin-nav">
|
|
<button class="nav-pill active" data-panel="kids">🧒 Kids</button>
|
|
<button class="nav-pill" data-panel="courses">📚 Courses</button>
|
|
<button class="nav-pill" data-panel="planner">🗓️ Planner</button>
|
|
<button class="nav-pill" data-panel="grading">✅ Grading</button>
|
|
<button class="nav-pill" data-panel="gradebook">📊 Gradebook</button>
|
|
<button class="nav-pill" data-panel="spelling">✏️ Spelling</button>
|
|
<button class="nav-pill" data-panel="decks">🗂️ Flashcards</button>
|
|
<button class="nav-pill" data-panel="settings">⚙️ Settings</button>
|
|
</nav>
|
|
</header>
|
|
|
|
<main class="admin-main">
|
|
|
|
<!-- Kids -->
|
|
<section id="panel-kids" class="panel">
|
|
<h1>Kids</h1>
|
|
<div class="panel-cols">
|
|
<div class="panel-col">
|
|
<div id="student-rows"></div>
|
|
</div>
|
|
<div class="panel-col">
|
|
<div class="admin-card">
|
|
<h2 style="margin:0 0 0.75rem">Add a kid</h2>
|
|
<form id="add-student-form" class="inline-form" style="flex-direction:column">
|
|
<input id="new-student-name" placeholder="Name" aria-label="New kid name">
|
|
<select id="new-student-emoji" aria-label="Avatar">
|
|
<option>🦊</option><option>🐸</option><option>🦄</option><option>🐯</option>
|
|
<option>🐼</option><option>🦖</option><option>🐙</option><option>🦉</option>
|
|
<option>🐻</option><option>🐰</option><option>🦁</option><option>🐶</option>
|
|
</select>
|
|
<button type="submit">Add kid</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Courses list -->
|
|
<section id="panel-courses" class="panel">
|
|
<h1>Courses</h1>
|
|
<div class="panel-cols">
|
|
<div class="panel-col">
|
|
<div id="course-rows"></div>
|
|
</div>
|
|
<div class="panel-col">
|
|
<div class="admin-card">
|
|
<h2 style="margin:0 0 0.75rem">Add a course</h2>
|
|
<form id="add-course-form" class="inline-form" style="flex-direction:column">
|
|
<input id="new-course-name" placeholder="Course name (e.g. Science 3)" aria-label="Course name">
|
|
<input id="new-course-subject" placeholder="Subject" aria-label="Subject">
|
|
<input id="new-course-color" type="color" value="#4f86f7" aria-label="Color">
|
|
<button type="submit">Add course</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Course detail (units + items + roster) -->
|
|
<section id="panel-course-detail" class="panel" hidden>
|
|
<button class="back-btn" data-nav="courses">← All courses</button>
|
|
<h1 id="cd-name"></h1>
|
|
|
|
<div class="admin-card">
|
|
<h2>Course info</h2>
|
|
<form id="course-meta-form" class="inline-form">
|
|
<input id="cd-name-input" placeholder="Course name">
|
|
<input id="cd-subject-input" placeholder="Subject">
|
|
<input id="cd-color-input" type="color">
|
|
<label class="checkbox-label"><input type="checkbox" id="cd-archived"> Archived</label>
|
|
<button type="submit">Save</button>
|
|
<button type="button" id="cd-duplicate" class="secondary">Duplicate course</button>
|
|
<button type="button" id="cd-delete" class="danger">Delete course</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="admin-card">
|
|
<h2>Roster</h2>
|
|
<div id="cd-roster"></div>
|
|
</div>
|
|
|
|
<div class="admin-card">
|
|
<h2>Units & content <button type="button" id="cd-print" class="secondary small" style="float:right">🖨 Print course</button></h2>
|
|
<div id="cd-units"></div>
|
|
<form id="add-unit-form" class="inline-form">
|
|
<input id="new-unit-name" placeholder="Unit name (e.g. Week 1: Weather)" aria-label="Unit name">
|
|
<button type="submit">Add unit</button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Item editor (lesson / assignment / quiz / spelling / flashcards) -->
|
|
<section id="panel-item-editor" class="panel" hidden>
|
|
<button class="back-btn" data-nav="course-detail">← Back to course</button>
|
|
<h1 id="ie-heading">New item</h1>
|
|
<form id="item-form" class="admin-card">
|
|
<input type="hidden" id="ie-id">
|
|
<input type="hidden" id="ie-unit-id">
|
|
|
|
<label>Type
|
|
<select id="ie-type">
|
|
<option value="lesson">📖 Lesson</option>
|
|
<option value="assignment">📝 Assignment</option>
|
|
<option value="quiz">❓ Quiz</option>
|
|
<option value="spelling_practice">✏️ Spelling Practice</option>
|
|
<option value="spelling_test">⭐ Spelling Test</option>
|
|
<option value="flashcards">🗂️ Flashcards</option>
|
|
</select>
|
|
</label>
|
|
<label>Title <input id="ie-title" placeholder="Item title"></label>
|
|
<div class="inline-form">
|
|
<label>Due date <input type="date" id="ie-due-date"></label>
|
|
<label class="checkbox-label"><input type="checkbox" id="ie-allow-retakes"> Allow retakes</label>
|
|
</div>
|
|
<label>Prerequisite item (must be done first)
|
|
<select id="ie-prereq"><option value="">None</option></select>
|
|
</label>
|
|
<div class="inline-form">
|
|
<label>Evidence from student
|
|
<select id="ie-evidence-mode">
|
|
<option value="none">None</option>
|
|
<option value="optional">Optional</option>
|
|
<option value="required">Required</option>
|
|
</select>
|
|
</label>
|
|
<label>Retake score policy
|
|
<select id="ie-retake-policy">
|
|
<option value="latest">Latest attempt</option>
|
|
<option value="highest">Highest score</option>
|
|
<option value="average">Average of all</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Scan a page: visible for lesson + quiz types -->
|
|
<div id="ie-scan-section" class="scan-card" hidden>
|
|
<strong>📷 Scan a book page</strong>
|
|
<input type="file" id="scan-file-input" accept="image/*" capture="environment" hidden>
|
|
<div class="scan-controls">
|
|
<button type="button" id="scan-pick-btn" class="secondary">Choose photo</button>
|
|
<button type="button" id="scan-go-btn" disabled>🔍 Scan this page</button>
|
|
</div>
|
|
<img id="scan-preview" alt="Selected page photo" hidden>
|
|
<p id="scan-status" class="hint"></p>
|
|
</div>
|
|
|
|
<!-- Import from Homeschool folder: visible for lesson, assignment, quiz types -->
|
|
<div id="ie-import-section" class="scan-card" hidden>
|
|
<strong>📂 Import from Homeschool folder</strong>
|
|
<div class="scan-controls">
|
|
<select id="import-file-select" style="flex:1"><option value="">— choose a file —</option></select>
|
|
<button type="button" id="import-load-btn" class="secondary">↓ Load files</button>
|
|
<button type="button" id="import-go-btn" class="secondary" disabled>📥 Import</button>
|
|
</div>
|
|
<p id="import-status" class="hint"></p>
|
|
</div>
|
|
|
|
<div id="ie-field-lesson" class="ie-field">
|
|
<div class="rte-label">Lesson content</div>
|
|
<div class="rte-toolbar">
|
|
<button type="button" class="rte-btn" data-cmd="bold" title="Bold"><b>B</b></button>
|
|
<button type="button" class="rte-btn" data-cmd="italic" title="Italic"><i>I</i></button>
|
|
<button type="button" class="rte-btn" data-cmd="underline" title="Underline"><u>U</u></button>
|
|
<span class="rte-sep"></span>
|
|
<button type="button" class="rte-btn" data-cmd="formatBlock" data-val="h2" title="Heading">H2</button>
|
|
<button type="button" class="rte-btn" data-cmd="formatBlock" data-val="h3" title="Sub-heading">H3</button>
|
|
<button type="button" class="rte-btn" data-cmd="formatBlock" data-val="p" title="Paragraph">¶</button>
|
|
<span class="rte-sep"></span>
|
|
<button type="button" class="rte-btn" data-cmd="insertUnorderedList" title="Bullet list">• List</button>
|
|
<button type="button" class="rte-btn" data-cmd="insertOrderedList" title="Numbered list">1. List</button>
|
|
<span class="rte-sep"></span>
|
|
<button type="button" class="rte-btn" id="rte-link-btn" title="Add link">🔗 Link</button>
|
|
<button type="button" class="rte-btn" id="rte-youtube-btn" title="Embed YouTube">▶ YouTube</button>
|
|
</div>
|
|
<div id="ie-body-lesson" class="rte-editor lesson-body" contenteditable="true"
|
|
data-placeholder="Weather is what the sky and air are doing right now. Add images, YouTube embeds, and more."></div>
|
|
</div>
|
|
|
|
<div id="ie-field-assignment" class="ie-field">
|
|
<label>Instructions <textarea id="ie-body-assignment" rows="5" placeholder="Draw today's weather and write one sentence."></textarea></label>
|
|
<label>Points possible <input id="ie-points" type="number" min="0" value="10"></label>
|
|
</div>
|
|
|
|
<div id="ie-field-quiz" class="ie-field">
|
|
<div class="inline-form" style="margin-bottom:.5rem">
|
|
<select id="ie-template-select"><option value="">— load from template —</option></select>
|
|
<button type="button" id="ie-load-template" class="secondary">Load</button>
|
|
<button type="button" id="ie-save-template" class="secondary">Save as template</button>
|
|
</div>
|
|
<div id="ie-questions"></div>
|
|
<button type="button" id="ie-add-question" class="secondary">+ Add question</button>
|
|
</div>
|
|
|
|
<div id="ie-field-spelling" class="ie-field">
|
|
<label>Word list <select id="ie-list"></select></label>
|
|
</div>
|
|
|
|
<div id="ie-field-flashcards" class="ie-field">
|
|
<label>Deck <select id="ie-deck"></select></label>
|
|
</div>
|
|
|
|
<div class="row-actions">
|
|
<button type="submit">Save item</button>
|
|
<button type="button" id="ie-delete" class="danger" hidden>Delete item</button>
|
|
</div>
|
|
<p id="ie-error" class="error" hidden></p>
|
|
</form>
|
|
</section>
|
|
|
|
<!-- Planner -->
|
|
<section id="panel-planner" class="panel">
|
|
<h1>Planner</h1>
|
|
<div class="inline-form">
|
|
<label>Kid <select id="planner-student"></select></label>
|
|
<button id="planner-view-week" class="secondary active-view">Week</button>
|
|
<button id="planner-view-month" class="secondary">Month</button>
|
|
<button id="planner-prev" class="secondary">← Prev</button>
|
|
<span id="planner-week-label" class="week-label"></span>
|
|
<button id="planner-next" class="secondary">Next →</button>
|
|
<button id="planner-copy" class="secondary">Copy last week</button>
|
|
<button id="planner-print">🖨 Print</button>
|
|
</div>
|
|
|
|
<details id="auto-schedule-details" class="admin-card" style="margin-bottom:1rem">
|
|
<summary style="cursor:pointer;font-weight:600">⚡ Auto-schedule a course</summary>
|
|
<div class="inline-form" style="margin-top:.75rem">
|
|
<label>Course <select id="auto-course"></select></label>
|
|
<label>Start date <input type="date" id="auto-start-date"></label>
|
|
<label>Items/day <input type="number" id="auto-items-per-day" value="1" min="1" max="10" style="width:5rem"></label>
|
|
<button id="auto-schedule-btn">Schedule</button>
|
|
</div>
|
|
<p id="auto-schedule-msg" class="hint" style="margin-top:.5rem"></p>
|
|
</details>
|
|
|
|
<div id="planner-grid" class="planner-grid"></div>
|
|
</section>
|
|
|
|
<!-- Grading queue -->
|
|
<section id="panel-grading" class="panel">
|
|
<h1>Grading Queue</h1>
|
|
<div id="grading-rows"></div>
|
|
<p id="grading-empty" class="hint" hidden>Nothing waiting to be graded 🎉</p>
|
|
<!-- Evidence lightbox -->
|
|
<div id="evidence-lightbox" class="modal" hidden>
|
|
<div class="modal-box">
|
|
<h3>Evidence</h3>
|
|
<div id="evidence-lightbox-body"></div>
|
|
<button id="evidence-lightbox-close" class="secondary">Close</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Gradebook -->
|
|
<section id="panel-gradebook" class="panel">
|
|
<h1>Gradebook</h1>
|
|
<div class="inline-form">
|
|
<label>Course <select id="gradebook-course"></select></label>
|
|
<button id="gradebook-csv-btn" class="secondary">⬇ Export CSV</button>
|
|
</div>
|
|
<div id="gradebook-table"></div>
|
|
<h2>Spelling Test History</h2>
|
|
<div id="spelling-tests-table"></div>
|
|
<div id="history-modal" class="modal" hidden>
|
|
<div class="modal-box">
|
|
<h3 id="history-modal-title">Attempt History</h3>
|
|
<div id="history-modal-body"></div>
|
|
<button id="history-modal-close" class="secondary">Close</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Spelling (legacy word lists + weekly assignment) -->
|
|
<section id="panel-spelling" class="panel">
|
|
<h1>Spelling</h1>
|
|
<div class="panel-cols">
|
|
<div class="panel-col">
|
|
<h2>This week's list per kid</h2>
|
|
<div id="assign-rows"></div>
|
|
<h2>Word lists</h2>
|
|
<div class="filter-bar">
|
|
<input id="list-filter" type="search" placeholder="Filter lists…" aria-label="Filter lists">
|
|
<button type="button" id="list-show-hidden" class="secondary small" hidden>Show hidden</button>
|
|
</div>
|
|
<div id="list-rows"></div>
|
|
</div>
|
|
<div class="panel-col">
|
|
<div class="toggle-btns">
|
|
<button type="button" id="list-new-btn" class="secondary">+ New list</button>
|
|
<button type="button" id="list-bulk-btn" class="secondary">⬆ Bulk import</button>
|
|
</div>
|
|
<div id="list-editor-section" hidden>
|
|
<h3 id="list-editor-title" class="section-form-title">New list</h3>
|
|
<form id="list-form" class="admin-card">
|
|
<input type="hidden" id="list-id">
|
|
<input id="list-name" placeholder="List name (e.g. Week of July 6)" aria-label="List name">
|
|
<input id="list-group" placeholder="Group (e.g. Bible 6)" aria-label="Group" list="list-groups-datalist">
|
|
<datalist id="list-groups-datalist"></datalist>
|
|
<textarea id="list-words" rows="8"
|
|
placeholder="One word per line. Paste any list from your curriculum! Add an example sentence with | receive | Did you receive my letter? Or just a plain list: because enough friend"></textarea>
|
|
<div class="row-actions">
|
|
<button type="submit">Save list</button>
|
|
<button type="button" id="list-cancel" class="secondary">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div id="list-bulk-section" hidden>
|
|
<h3 class="section-form-title">Bulk import lists</h3>
|
|
<div class="admin-card">
|
|
<p class="hint" style="margin:0 0 0.5rem">Start each list with <strong># List name</strong> on its own line, then words below it. Optional group applies to all imported lists.</p>
|
|
<input id="list-bulk-group" placeholder="Group (e.g. Bible 6) — optional" aria-label="Group for bulk import" list="list-groups-datalist">
|
|
<textarea id="list-bulk-text" rows="12"
|
|
placeholder="# Week 1 because enough friend # Week 2 receive | Did you receive my letter? achieve believe"></textarea>
|
|
<div class="row-actions">
|
|
<button type="button" id="list-bulk-save">Import all lists</button>
|
|
<button type="button" id="list-bulk-clear" class="secondary">Clear</button>
|
|
</div>
|
|
<p id="list-bulk-status" class="hint" style="margin:0.4rem 0 0"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Flashcard decks -->
|
|
<section id="panel-decks" class="panel">
|
|
<h1>Flashcard Decks</h1>
|
|
<div class="panel-cols">
|
|
<div class="panel-col">
|
|
<div class="filter-bar">
|
|
<input id="deck-filter" type="search" placeholder="Filter decks…" aria-label="Filter decks">
|
|
</div>
|
|
<div id="deck-rows"></div>
|
|
</div>
|
|
<div class="panel-col">
|
|
<div class="toggle-btns">
|
|
<button type="button" id="deck-new-btn" class="secondary">+ New deck</button>
|
|
<button type="button" id="deck-bulk-btn" class="secondary">⬆ Bulk import</button>
|
|
</div>
|
|
<div id="deck-editor-section" hidden>
|
|
<h3 id="deck-editor-title" class="section-form-title">New deck</h3>
|
|
<form id="deck-form" class="admin-card">
|
|
<input type="hidden" id="deck-id">
|
|
<input id="deck-name" placeholder="Deck name (e.g. State Capitals)" aria-label="Deck name">
|
|
<input id="deck-group" placeholder="Group (e.g. Bible 6)" aria-label="Group" list="deck-groups-datalist">
|
|
<datalist id="deck-groups-datalist"></datalist>
|
|
<textarea id="deck-cards" rows="8"
|
|
placeholder="One card per line: front | back What is the capital of Ohio? | Columbus"></textarea>
|
|
<div class="row-actions">
|
|
<button type="submit">Save deck</button>
|
|
<button type="button" id="deck-cancel" class="secondary">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div id="deck-bulk-section" hidden>
|
|
<h3 class="section-form-title">Bulk import decks</h3>
|
|
<div class="admin-card">
|
|
<p class="hint" style="margin:0 0 0.5rem">Start each deck with <strong># Deck name</strong> on its own line, then cards below as <strong>front | back</strong>. Optional group applies to all imported decks.</p>
|
|
<input id="deck-bulk-group" placeholder="Group (e.g. Bible 6) — optional" aria-label="Group for bulk import" list="deck-groups-datalist">
|
|
<textarea id="deck-bulk-text" rows="12"
|
|
placeholder="# Chapter 1 Vocab What is photosynthesis? | The process plants use to make food What does chlorophyll do? | Absorbs light # Chapter 2 Vocab What is mitosis? | Cell division producing identical cells"></textarea>
|
|
<div class="row-actions">
|
|
<button type="button" id="deck-bulk-save">Import all decks</button>
|
|
<button type="button" id="deck-bulk-clear" class="secondary">Clear</button>
|
|
</div>
|
|
<p id="deck-bulk-status" class="hint" style="margin:0.4rem 0 0"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Settings -->
|
|
<section id="panel-settings" class="panel">
|
|
<h1>Settings</h1>
|
|
<form id="pin-change-form" class="inline-form">
|
|
<input id="new-pin" type="password" inputmode="numeric" placeholder="New PIN (4+ digits)" aria-label="New PIN">
|
|
<button type="submit">Change PIN</button>
|
|
</form>
|
|
|
|
<h2>Page scanning (AI vision)</h2>
|
|
<p class="hint">Used by "Scan a page" in the lesson/quiz editor. Configure one or both providers — the primary is tried first and the other is used as automatic fallback.</p>
|
|
<form id="scan-config-form">
|
|
<fieldset class="scan-provider-box">
|
|
<legend>Primary provider</legend>
|
|
<label class="radio-label">
|
|
<input type="radio" name="scan_primary" value="local"> Local (Ollama) — runs on your machine, completely free & private
|
|
</label>
|
|
<label class="radio-label">
|
|
<input type="radio" name="scan_primary" value="ollama_cloud"> Ollama Cloud — free hosted tier, requires API key
|
|
</label>
|
|
</fieldset>
|
|
|
|
<fieldset class="scan-provider-box">
|
|
<legend>Local — Ollama</legend>
|
|
<div class="inline-form">
|
|
<input id="ollama-host" placeholder="http://localhost:11434" aria-label="Ollama host">
|
|
<input id="ollama-model" placeholder="llava" aria-label="Vision model">
|
|
<button type="button" id="ollama-test-btn" class="secondary">Test</button>
|
|
</div>
|
|
<p id="ollama-test-result" class="hint"></p>
|
|
<p class="hint">Install a vision model: <code>ollama pull llava</code> or <code>ollama pull llava-llama3</code></p>
|
|
</fieldset>
|
|
|
|
<fieldset class="scan-provider-box">
|
|
<legend>Ollama Cloud (free tier)</legend>
|
|
<div class="inline-form">
|
|
<input id="ollama-cloud-model" placeholder="gemma4" aria-label="Ollama Cloud model">
|
|
<input id="ollama-cloud-api-key" type="password" placeholder="Ollama Cloud API key" aria-label="Ollama Cloud API key">
|
|
<span id="ollama-cloud-key-status" class="hint"></span>
|
|
</div>
|
|
<p class="hint">Runs through your local Ollama but offloads compute to Ollama's cloud. Get a free API key at <strong>ollama.com</strong> → Account → API Keys, then pull the model: <code>ollama pull gemma4:cloud</code>. Enter the base name only (e.g. <code>gemma4</code>) — <code>:cloud</code> is added automatically.</p>
|
|
</fieldset>
|
|
|
|
<button type="submit">Save scan settings</button>
|
|
</form>
|
|
</section>
|
|
|
|
<p id="admin-msg" class="hint"></p>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Floating assign popup -->
|
|
<div id="assign-popup" hidden>
|
|
<p class="assign-popup-label">Assign to…</p>
|
|
<div id="assign-popup-students"></div>
|
|
</div>
|
|
|
|
<script src="admin.js"></script>
|
|
</body>
|
|
</html>
|