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 page
  • homeschool-login.html
  • student-picker.html — the manifest start_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'))