Skip to content
torwagerPublic

About

Schoology task and homework scheduler

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Homework — Schoology → study plan PWA

A tiny personal homework manager: a Cloudflare Worker polls a Schoology calendar feed — and, optionally, a Spond soccer team and a Google family calendar — every 30 minutes, stores assignments in KV, diffs each sync (so late-night postings show up under New), and serves a Progressive Web App that gives a daily study plan, an upcoming-due-dates list across all classes, and per-class views with check-offs.

Design background and the hosting/PWA comparison are in docs/design-notes.md.

src/worker.js    fetch handler (/api/*) + scheduled handler (cron sync)
src/ics.js       dependency-free iCalendar parser (Schoology + general calendars)
src/recur.js     RRULE expansion, for calendars that repeat events by rule
src/spond.js     Spond client (unofficial API) for the soccer schedule
src/courses.js   class tagging: manual tags + keyword rules (feed has no course field)
src/planner.js   earliest-deadline-first study planner
public/          the PWA (index.html, app.js, styles.css, manifest, sw.js, icons)
test/            node:test unit tests   → npm test
wrangler.jsonc   Cloudflare config (KV binding, cron, static assets)

⚠️ Secrets

The Schoology iCal URL is a bearer secret: anyone who has it can read the calendar. It lives only in Cloudflare secrets (ICS_URL) and in the git-ignored .dev.vars for local dev. Never paste it into code, wrangler.jsonc, an issue, or a commit. This repo is public on GitHub.

If you enable the Spond sync, the Spond account password is a Cloudflare secret too (SPOND_PASSWORD). The Google Calendar secret iCal address (GCAL_ICS_URL) is likewise a bearer credential — anyone holding it can read the family calendar. Same rule for all of them: never in a file, never in a commit.

The app itself is gated by a passphrase (APP_TOKEN). The static HTML shell is public; all data endpoints under /api/* require the passphrase.


Setup walkthrough (Cloudflare free account)

0. Prerequisites (one-time, on your Mac)

  • Node ≥ 20 (you have 25) and npm install in this folder (already done — installs wrangler).
  • A Cloudflare account: https://dash.cloudflare.com/sign-up (free plan; no card needed).

1. Log wrangler into Cloudflare

! npx wrangler login

A browser tab opens; click Allow. Verify with npx wrangler whoami.

2. Create the KV namespace

npx wrangler kv namespace create SCHOOLOGY_KV

It prints something like:

{ binding = "SCHOOLOGY_KV", id = "0123abcd…" }

Copy the id into wrangler.jsonc, replacing REPLACE_WITH_KV_ID.

3. Set the two secrets

npx wrangler secret put ICS_URL

Paste the feed URL with https:// instead of webcal:// (from your private note), press Enter.

npx wrangler secret put APP_TOKEN

Type a passphrase she'll enter once on her phone (something memorable but long, e.g. four words). Note: secret put needs the Worker to exist — if it complains, run step 4 first, then come back.

4. Deploy

npm run deploy

Output ends with the public URL, e.g. https://schoology-homework.<your-subdomain>.workers.dev. The cron trigger (*/30 * * * *) is registered automatically.

5. First sync and check

The cron runs at the next :00/:30. To sync immediately, open the app in a browser, enter the passphrase, go to ⚙︎ → Sync now, or:

curl -X POST -H "authorization: Bearer YOUR_PASSPHRASE" https://schoology-homework.<sub>.workers.dev/api/sync
curl -H "authorization: Bearer YOUR_PASSPHRASE" https://schoology-homework.<sub>.workers.dev/api/health

health should show itemCount in the hundreds and lastError: null. To watch the cron live: npm run tail.

6. Install on her iPhone

  1. Open the URL in Safari (must be Safari, not Chrome, for home-screen install).
  2. Enter the passphrase. It's stored on the phone only.
  3. Tap Share → Add to Home Screen → Add. The "Homework" icon appears; it launches full-screen like an app.
  4. Optional: also add the same iCal URL to Apple Calendar (Settings → Calendar → Accounts → Add Subscribed Calendar) so due dates show up in the phone's native calendar too.

7. (Optional, recommended) Custom domain or extra lock-down

  • Nicer URL: if you own a domain on Cloudflare, add a route in the dashboard (Workers → your worker → Settings → Domains & Routes).
  • Stronger gate: Cloudflare Zero Trust → Access (free for up to 50 users) can put a Google-login wall in front of the whole site. Then the passphrase becomes a second layer. Not needed to start.

Soccer schedule (Spond)

Spond has no iCal feed and no public API — their only calendar feature is a one-way push into the phone's native calendar, and only for events you're personally invited to. So src/spond.js talks to the same undocumented endpoints the Spond app uses:

POST https://api.spond.com/core/v1/auth2/login   {email, password} -> {accessToken:{token, expiration}}
GET  https://api.spond.com/core/v1/sponds/?…     Bearer token      -> [event, …]

Caveats worth knowing before you turn it on:

  • Unofficial and unversioned. Spond can change or block it without notice.
  • 2FA breaks it. If the account has two-factor auth enabled, login fails; there's no TOTP flow.
  • It needs the account password as a Cloudflare secret. Use the parent account that already sees her team.
  • Only events that account is invited to are visible — same limit as Spond's own calendar sync.

If any of that goes bad, nothing else breaks: each source syncs independently, a failing source keeps its last-known items, and the error shows up per-source in ⚙︎ Settings → Sync.

Turning it on

npx wrangler secret put SPOND_EMAIL
npx wrangler secret put SPOND_PASSWORD
npm run deploy

Optionally pin it to one team, so you don't pick up every group the account belongs to. List the ids:

curl -H "authorization: Bearer YOUR_PASSPHRASE" https://schoology-homework.<sub>.workers.dev/api/spond/groups

then set the one you want:

npx wrangler secret put SPOND_GROUP_ID

SPOND_LABEL (a plain var, not a secret) is the chip label used when an event carries no group name; it defaults to Soccer.

How soccer shows up

Spond events are stored with kind: "event" and source: "spond", which means:

  • They appear in Today and Tomorrow as a calendar card above the study plan, and in Upcoming alongside school events — with start–end time, location, and a link to the event in Spond.
  • Cancelled events are struck through and marked, and a cancellation lands in the Changes log like any other edit.
  • The planner ignores them. Daily study capacity is unchanged by practices and games (src/planner.js skips kind === "event"), and they never appear in the Classes tab.

Family calendar (Google)

Google's secret iCal address is a private feed URL, so this needs no OAuth and no API key — it reuses the same iCal path as Schoology.

Getting the address

  1. Google Calendar on the web → hover the family calendar in the left sidebar → ⋮ → Settings and sharing.
  2. Scroll to Integrate calendar.
  3. Copy Secret address in iCal format (it ends in /basic.ics). Use the secret one, not the public one — the calendar isn't publicly shared.

Treat it like a password: anyone with that URL can read the calendar. If it ever leaks, Reset it on that same settings page.

npx wrangler secret put GCAL_ICS_URL
npm run deploy

GCAL_LABEL (a plain var, default Family) is the chip label, and GCAL_DAYS (default 120) is how far ahead to expand.

Two things Google's feed does that Schoology's doesn't

  • Times come as wall clock in a named zone (DTSTART;TZID=America/New_York:20260903T170000) rather than UTC. zonedToIso in src/ics.js resolves those, re-checking the offset so a 5pm event stays at 5pm on both sides of a daylight-saving change.
  • Repeating events are sent once, with an RRULE — Google does not list each occurrence. src/recur.js expands them across the sync window, honouring INTERVAL, COUNT, UNTIL, BYDAY (including ordinals like 3TU), BYMONTHDAY, EXDATE, and the separate RECURRENCE-ID events Google emits for a single moved instance. Without this a weekly dinner would appear exactly once, on the day it was created.

How family events show up

Like soccer, they're kind: "event" — visible on the Today/Tomorrow calendar card and in Upcoming, invisible to the study planner and the Classes tab.

They deliberately don't appear in Changes. Occurrences are generated into a rolling window, so each day one enters and one leaves; logging that would bury real homework postings under daily churn (LOGGED_SOURCES in src/worker.js).

Local development

cp .dev.vars.example .dev.vars   # then fill in ICS_URL and a dev APP_TOKEN
npm run dev                      # http://localhost:8787
npm run sync                     # trigger the cron locally
npm test

wrangler dev uses a local KV simulator, so nothing touches production.

How it works

  • Sync (src/worker.js → sync): every 30 minutes the cron trigger runs each configured source — Schoology (.ics → parseSchoologyFeed), plus Spond (pullSpond) and Google Calendar (parseCalendarFeed) if their secrets are set — merges the results, compares them to the previous snapshot by uid, appends new / changed / cancelled / removed entries to the change log, and stores items + meta in KV. Sources are isolated: one failing leaves the other's items alone and is diffed as nothing-changed rather than mass deletion, so an outage never floods the log with false removals. The very first sync doesn't flood the log either.
  • Items carry source (schoology / spond / gcal), kind (assignment / assessment / discussion / event, from the Schoology URL), course, title, due ({date, allDay:true} or {iso, allDay:false}), plus the raw summary and description.
  • Planner (src/planner.js): open, undone work items sorted by due date; each day gets a capacity (weekday/weekend minutes from Settings); big items are spread in chunks with the remainder allowed near the due date; anything that can't fit before its due date is listed as "Doesn't fit".
  • State (check-offs, per-item effort estimates, settings) is stored in KV so it syncs across devices.
  • PWA: sw.js caches the shell for instant/offline launch; app.js caches the last API responses in localStorage so the app still shows data when offline.

Classes (which assignment belongs to which course)

The Schoology feed carries no course information for assignments — just title, description, and a link (verified against real entries on 2026-08-28; the assignment pages themselves sit behind Google sign-in). So the app tags classes itself, in two layers (src/courses.js):

  1. Manual tag — tap the class chip on any assignment and pick a class (or "+ New class…"). Stored in KV (state.courseOf), so it syncs across devices.
  2. Keyword rules — in ⚙︎ Settings → Classes, give each class comma-separated keywords (e.g. social studies, silk route). Any assignment whose title or description contains a keyword is tagged automatically. Manual tags always win.

Anything unmatched shows as Unassigned, listed first in the Classes tab.

Next steps (not built yet)

  • Push notifications for new/changed assignments (Web Push with VAPID keys from the Worker; works on iOS 16.4+ for home-screen PWAs).
  • LLM pass for effort estimation from titles / backward planning for tests.
  • Weekly "what got done" retro view.

About

Schoology task and homework scheduler

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages