📊 Data Structures
localStorage schemas and object formats used in HLMS.
localStorage Keys
| Key | Type | Description |
|---|---|---|
studentSubmissions |
Array | All submitted assignments |
userNotifications |
Array | Student notifications |
teacherNotifications |
Array | Teacher notifications |
assignmentDrafts |
Object | Auto-saved work in progress |
completedAssignments |
Array | IDs of completed assignments |
homeschoolSession |
Object | Who is signed in on this device. Read and written only through session-auth.js. |
Session Object
The session has no expiry field and no page enforces one. It is valid for as long as it exists, and only Log Out removes it. Do not reintroduce expiresAt, an inactivity timer, or a Session Timeout setting.
// Teacher / parent
{
role: "teacher", // "teacher" | "parent" | "student"
name: "Seth Andrews",
loginTime: "2026-08-26T16:53:20.747Z",
sessionId: "c38b3888-fc37-48d5-b9cc-2caacb18a7cd"
}
// Student (written by student-picker.html)
{
role: "student",
studentId: "<uuid>",
name: "Lucas",
avatar: "👦",
loginTime: "2026-08-26T16:53:20.747Z",
sessionId: "student-1756227200747"
}
session-auth.js
Every page that touches the session loads <script src="session-auth.js"></script> in its <head>, before its own scripts, and goes through the helper rather than reading storage directly.
| Call | Behavior |
|---|---|
HLMSAuth.getSession() |
Returns the session object, or null when it is missing, unparseable, or carries neither role nor studentId. Never throws, so corrupt data sends the user to login instead of breaking the page. |
HLMSAuth.setSession(obj) |
Writes the session and the hlms_authenticated gate flag. |
HLMSAuth.clearSession() |
Removes the session plus hlms_authenticated, hlms_auth_time, rememberDevice, studentId, studentName, studentAvatar. Leaves homeschoolConfig alone, so logging out never resets the teacher PIN. |
On load the helper runs a one time migration: a session found in sessionStorage with none in localStorage is copied across and the legacy copy removed, so moving to persistent storage does not sign anyone out.
shared-nav.js and shared/universal-nav.html call the helper when present and fall back to removing the keys directly, because the nav fragment can be injected into a page that has not loaded it.
Resume breadcrumb
The PWA manifest start_url is /dashboards/student-picker.html and is not changed: installed apps already cached it, and changing it would mean reinstalling on every device. Resume is handled on the page instead.
session-auth.js writes hlmsLastPage to localStorage holding pathname + search. The query string matters: teacher pages need the ?parent= they were opened with.
Recording is deferred rather than done at load, on a 1200ms timer plus pagehide and visibilitychange. Every auth check in the app runs synchronously or on DOMContentLoaded, so by the time these fire a page that was going to reject the user has already navigated away and never becomes the resume target. Pages in the never-record list (the start page, both login pages, the family picker, index.html, offline.html) are skipped, as is any path that is not under /dashboards/.
| Call | Behavior |
|---|---|
HLMSAuth.getRoleHome(session) |
Fallback landing page. Teacher goes to family-picker.html, because every teacher page requires a ?parent= plus a matching verified_<id> flag and the family picker is what issues both. Parent goes to baseball-stats.html. Student goes to student-dashboard.html?studentId=…. Returns null for a student session with no studentId, which is the state right after login, so the picker renders instead. |
HLMSAuth.resumeTarget(session) |
Where the launch page should send this session, or null to render normally. |
recordLastPage / getLastPage / clearLastPage |
Direct breadcrumb access. clearSession() drops the breadcrumb too. |
Loop guard. resumeTarget stores the path it attempted in sessionStorage under hlmsResumeTried, which is empty on every fresh launch. Arriving back at the start page with the breadcrumb still pointing at that same path means the page rejected the user and bounced straight back, so the entry is dropped and the role home used instead. If the breadcrumb has moved on the user genuinely got somewhere, and a later visit resumes normally rather than being punished for the earlier redirect.
Switch Student. ?switch=1 skips resume entirely and renders the page. Without it the Switch Student link on the student dashboard would bounce straight back and there would be no way to change child. The same parameter is how Back to Login on the picker reaches the login form while a session is still live.
Entry points
Three pages can receive a cold launch, and all three call HLMSAuth.routeOnLoad() once, before rendering, so there is one implementation of where a session belongs rather than three that drift apart:
index.html— the site root redirects here, so an install started from the root captures this pagehomeschool-login.htmlstudent-picker.html— the manifeststart_url
routeOnLoad() honours ?switch=1, calls resumeDecision(), stamps and stores the decision for the diagnostics readout, and redirects when the target is non-null. A null target means render the page, which is what a visitor with no session gets. It never throws: a routing failure records itself as the reason and renders.
All three are in the never-record list, so none can become a breadcrumb, and the marker handshake works across them: a resume started from index.html whose target bounces is caught by whichever entry point the user lands on next.
Manifest. start_url is /dashboards/student-picker.html and scope is /dashboards/, set explicitly rather than left to default from start_url. index.html carries the manifest link and the PWA meta tags so an install begun at the root resolves to the real start URL instead of capturing the landing page.
Submission Object
{
// Unique identifiers
id: 'submission_1764001677508_um6scxa6x',
assignmentId: 'writing',
// Student info
studentName: 'Lucas Andrews',
// Assignment details
subject: 'Writing',
type: 'writing',
title: 'My Favorite Sport',
// Content (varies by type)
content: '<p>Baseball is my favorite sport...</p>',
writtenWork: '<p>Alternative storage...</p>',
canvasData: 'data:image/png;base64,...', // Math drawings
photos: ['data:image/jpeg;base64,...'], // Uploaded photos
// Reflections (required)
reflection: {
interesting: 'Learning about writing paragraphs',
hard: 'Thinking of enough sentences',
questions: 'How can I make my writing better?'
},
// Metadata
submittedAt: '2025-11-24T15:51:55.057Z',
timeSpent: 20, // minutes
wordCount: 108,
// Status tracking
status: 'pending' | 'returned' | 'graded',
reviewed: false,
// Teacher feedback (after review)
teacherFeedback: 'Great work Lucas! Your paragraph...',
grade: 'A',
areasOfStrength: ['Computation accuracy', 'Persistence/effort'],
areasNeedingPractice: ['Multi-step problems'],
followUpAction: 'Brief review in next lesson',
// Timestamps
returnedAt: '2025-11-24T16:30:00.000Z',
gradedAt: '2025-11-24T17:00:00.000Z',
// Follow-up
followUpNeeded: false,
followUpDate: '2025-11-25',
followUpTopic: 'Comma usage mini-lesson'
}
Notification Object
{
id: 'notif_' + Date.now(),
timestamp: '2025-11-24T15:51:55.057Z',
read: false,
title: '📝 Assignment Returned',
message: 'Your Math assignment has been reviewed. Check your feedback!',
action: {
type: 'viewAssignment',
assignmentId: 'math'
}
}
Notification Types
notif_graded_* |
Assignment received final grade |
notif_revision_* |
Assignment returned for revision |
notif_returned_* |
Teacher confirmation of return |
Assignment Draft Object
// Key: assignmentId
{
"writing": {
content: "<p>Work in progress...</p>",
reflection: {
interesting: "",
hard: "",
questions: ""
},
lastSaved: "2025-11-24T15:45:00.000Z"
},
"math": {
canvasData: "data:image/png;base64,...",
reflection: {
interesting: "The multiplication",
hard: "",
questions: ""
},
lastSaved: "2025-11-24T15:50:00.000Z"
}
}
ID Generation
// Submission ID format
id: 'submission_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
// Example: 'submission_1764001677508_um6scxa6x'
// Notification ID format
id: 'notif_' + Date.now()
// Example: 'notif_1764001677508'
// With type prefix
id: 'notif_graded_' + Date.now()
id: 'notif_revision_' + Date.now()
Status Values
| Status | Meaning | Student Sees |
|---|---|---|
pending |
Awaiting teacher review | Yellow "Pending" badge |
returned |
Sent back for revision | Red "Try Again" card |
graded |
Complete with grade | Green "Graded" card with letter |
Content by Subject Type
| Subject | Content Field | Format |
|---|---|---|
| Writing | content or writtenWork |
HTML from Quill |
| Reading | content |
HTML from Quill |
| Science | content |
HTML from Quill |
| Spelling | content |
HTML from Quill |
| Math | canvasData |
Base64 PNG image |
Debugging localStorage
// View all submissions
JSON.parse(localStorage.getItem('studentSubmissions'))
// View specific submission
const subs = JSON.parse(localStorage.getItem('studentSubmissions'));
subs.find(s => s.assignmentId === 'math')
// Clear all data (reset)
localStorage.clear()
// View notifications
JSON.parse(localStorage.getItem('userNotifications'))
JSON.parse(localStorage.getItem('teacherNotifications'))