lastbell 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. lastbell-0.1.0/LICENSE +21 -0
  2. lastbell-0.1.0/PKG-INFO +424 -0
  3. lastbell-0.1.0/README.md +388 -0
  4. lastbell-0.1.0/lastbell/__init__.py +7 -0
  5. lastbell-0.1.0/lastbell/app.js +439 -0
  6. lastbell-0.1.0/lastbell/cli.py +649 -0
  7. lastbell-0.1.0/lastbell/client.py +235 -0
  8. lastbell-0.1.0/lastbell/collector.py +144 -0
  9. lastbell-0.1.0/lastbell/config.py +106 -0
  10. lastbell-0.1.0/lastbell/dashboard.py +2090 -0
  11. lastbell-0.1.0/lastbell/differ.py +212 -0
  12. lastbell-0.1.0/lastbell/favicon.png +0 -0
  13. lastbell-0.1.0/lastbell/gradebook.py +338 -0
  14. lastbell-0.1.0/lastbell/mcps_schools.json +2116 -0
  15. lastbell-0.1.0/lastbell/models.py +112 -0
  16. lastbell-0.1.0/lastbell/notify/__init__.py +125 -0
  17. lastbell-0.1.0/lastbell/notify/console.py +7 -0
  18. lastbell-0.1.0/lastbell/notify/email.py +93 -0
  19. lastbell-0.1.0/lastbell/notify/ntfy.py +45 -0
  20. lastbell-0.1.0/lastbell/notify/pushover.py +39 -0
  21. lastbell-0.1.0/lastbell/notify/telegram.py +39 -0
  22. lastbell-0.1.0/lastbell/outbox.py +178 -0
  23. lastbell-0.1.0/lastbell/paths.py +77 -0
  24. lastbell-0.1.0/lastbell/preflight.py +429 -0
  25. lastbell-0.1.0/lastbell/router.py +133 -0
  26. lastbell-0.1.0/lastbell/schema.sql +125 -0
  27. lastbell-0.1.0/lastbell/schools.py +138 -0
  28. lastbell-0.1.0/lastbell/secrets.py +83 -0
  29. lastbell-0.1.0/lastbell/seed.py +264 -0
  30. lastbell-0.1.0/lastbell/setup_wizard.py +397 -0
  31. lastbell-0.1.0/lastbell/store.py +236 -0
  32. lastbell-0.1.0/lastbell/style.css +1099 -0
  33. lastbell-0.1.0/lastbell/summary.py +159 -0
  34. lastbell-0.1.0/lastbell/watchers.py +400 -0
  35. lastbell-0.1.0/lastbell.egg-info/PKG-INFO +424 -0
  36. lastbell-0.1.0/lastbell.egg-info/SOURCES.txt +58 -0
  37. lastbell-0.1.0/lastbell.egg-info/dependency_links.txt +1 -0
  38. lastbell-0.1.0/lastbell.egg-info/entry_points.txt +3 -0
  39. lastbell-0.1.0/lastbell.egg-info/requires.txt +9 -0
  40. lastbell-0.1.0/lastbell.egg-info/top_level.txt +1 -0
  41. lastbell-0.1.0/pyproject.toml +58 -0
  42. lastbell-0.1.0/setup.cfg +4 -0
  43. lastbell-0.1.0/tests/test_config.py +80 -0
  44. lastbell-0.1.0/tests/test_dashboard.py +1109 -0
  45. lastbell-0.1.0/tests/test_differ.py +97 -0
  46. lastbell-0.1.0/tests/test_gradebook.py +75 -0
  47. lastbell-0.1.0/tests/test_outbox.py +154 -0
  48. lastbell-0.1.0/tests/test_paths.py +53 -0
  49. lastbell-0.1.0/tests/test_phase4.py +159 -0
  50. lastbell-0.1.0/tests/test_preflight.py +208 -0
  51. lastbell-0.1.0/tests/test_refresh_schools.py +68 -0
  52. lastbell-0.1.0/tests/test_rollover.py +153 -0
  53. lastbell-0.1.0/tests/test_router.py +135 -0
  54. lastbell-0.1.0/tests/test_schools.py +87 -0
  55. lastbell-0.1.0/tests/test_seed.py +89 -0
  56. lastbell-0.1.0/tests/test_setup.py +237 -0
  57. lastbell-0.1.0/tests/test_store.py +123 -0
  58. lastbell-0.1.0/tests/test_summary.py +105 -0
  59. lastbell-0.1.0/tests/test_time_rules.py +129 -0
  60. lastbell-0.1.0/tests/test_watchers.py +175 -0
lastbell-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Hays
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,424 @@
1
+ Metadata-Version: 2.4
2
+ Name: lastbell
3
+ Version: 0.1.0
4
+ Summary: Self-hosted ParentVUE grade & assignment monitor — alerts for any number of watchers, and the students too.
5
+ Author: Last Bell contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/noestudios/lastbell
8
+ Project-URL: Repository, https://github.com/noestudios/lastbell
9
+ Project-URL: Issues, https://github.com/noestudios/lastbell/issues
10
+ Keywords: parentvue,edupoint,synergy,grades,school,self-hosted
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Education
24
+ Classifier: Topic :: Home Automation
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: requests>=2.31
29
+ Requires-Dist: python-dotenv>=1.0
30
+ Requires-Dist: keyring>=24.0
31
+ Provides-Extra: service
32
+ Requires-Dist: APScheduler>=3.10; extra == "service"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7.4; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # Last Bell
38
+
39
+ A self-hosted **ParentVUE grade & assignment monitor**. It watches your own
40
+ students' gradebooks and pushes alerts — missing assignments, new or changed
41
+ grades, long-term work that's still ungraded or coming due — to **any number of
42
+ watchers** (guardians *and* the students themselves), on whatever device they
43
+ already use.
44
+
45
+ > Not affiliated with Edupoint. It uses **your** credentials to read **your**
46
+ > students' data, and everything runs on hardware you control. Before anything
47
+ > else, read [what it asks of the portal](#being-a-good-neighbor-to-the-portal)
48
+ > and [where your credentials and your students' data
49
+ > go](#credentials--student-data-the-actual-guarantees) — every sentence there
50
+ > is backed by linked code.
51
+
52
+ **Status: all roadmap phases complete.** `lastbell run` sweeps **every class** per
53
+ student (via each class row's own `data-focus` payload, the same drill-down
54
+ the portal UI performs), persists a snapshot keyed on the Edupoint assignment
55
+ GUID, diffs against the previous run, and alerts on **score changes,
56
+ missing-flags, work still ungraded past its due date, deadlines entering the
57
+ look-ahead window, and course grades dropping past a threshold** — plus a
58
+ one-shot **final-grades summary** when a marking period closes (the persisted
59
+ per-student term is the dedup; the closing term's last-seen marks are its
60
+ finals, the new term starts as a quiet fresh baseline, and the dashboard and
61
+ daily summaries scope themselves to the current term). Delivery is
62
+ per-watcher: **subscriptions** filtered by alert type over **channels**
63
+ (email/SMS-gateway, ntfy, Telegram, Pushover), each optionally batched into a
64
+ **daily digest**, held during **quiet hours**, or replaced by a generated
65
+ **daily summary** — plus a web **dashboard** to look things up on demand. Data
66
+ path verified live against MCPS (`md-mcps-psv.edupoint.com`, 2026-08-31).
67
+
68
+ ---
69
+
70
+ ## What it looks like
71
+
72
+ Every screenshot is from `lastbell seed-demo` — a fabricated family at
73
+ end-of-quarter volume, no real students.
74
+
75
+ **Home** — every student at a glance: each course with its current grade,
76
+ and the open-item badges that matter today.
77
+
78
+ ![Home: one card per student with courses, grades, and open-item badges](https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/home.png)
79
+
80
+ **The four tracking cards** — a student's page is a set of views, and the
81
+ cards are both the summary and the switch. Each carries its own data story:
82
+
83
+ ![The four tracking cards: Needs attention, Due soon, Recent grades, Everything](https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/stat-cards.png)
84
+
85
+ - **Needs attention** — work the teacher marked missing, plus anything
86
+ past due with no grade posted. The count, this week's change, and a
87
+ six-week trend line, so a bad week and a slow slide look different.
88
+ - **Due soon** — what's coming in the next 7 days (`LASTBELL_LOOKAHEAD_DAYS`),
89
+ with the next two named right on the card.
90
+ - **Recent grades** — the average of the last ten scores, shown as bars
91
+ against the term average: the fastest read on how things are going
92
+ *right now*, before it moves the course grade.
93
+ - **Everything** — the term average across all courses and its trend, and
94
+ the door into the full archive: every class, every assignment, closed
95
+ terms folded to their finals.
96
+
97
+ **A student's page** — the All Courses strip collapsed (the default; the
98
+ cards are the front door) and expanded (grade, two-week movement, open
99
+ items, last graded — each course name filters the view below to that class):
100
+
101
+ <p>
102
+ <img src="https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/student-collapsed.png" width="49%" alt="Student page with the All Courses strip collapsed">
103
+ <img src="https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/student-expanded.png" width="49%" alt="Student page with the All Courses strip expanded">
104
+ </p>
105
+
106
+ **Needs attention** — the default view: missing work first, then ungraded
107
+ past-due, each row tinted and iconed so the list scans by color before
108
+ it's read.
109
+
110
+ ![Needs attention panel: missing and ungraded-past-due assignments](https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/needs-attention-panel.png)
111
+
112
+ **Alerts and history** — Alerts is everything a watcher was told about,
113
+ grouped by type and paged; History is every grade and status change ever
114
+ seen, filterable by class and by kind of change.
115
+
116
+ <p>
117
+ <img src="https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/alerts.png" width="49%" alt="Alerts log with type badges and paging">
118
+ <img src="https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/history.png" width="49%" alt="Grade history with class and change-kind filters">
119
+ </p>
120
+
121
+ **Settings** — watchers (guardians *and* students) with their channels,
122
+ and subscriptions: who hears about which student, over which channel,
123
+ immediately or in a daily digest, with the urgent types allowed through
124
+ right away.
125
+
126
+ ![Settings: watchers, channels, and subscriptions](https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/settings.png)
127
+
128
+ **On a phone** — the same pages, no separate app: tables restack into
129
+ cards and the nav collapses to icons.
130
+
131
+ <p align="center">
132
+ <img src="https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/mobile-student.png" width="40%" alt="Student page on a phone">
133
+ &nbsp;&nbsp;
134
+ <img src="https://raw.githubusercontent.com/noestudios/lastbell/main/assets/screenshots/mobile-alerts.png" width="40%" alt="Alerts on a phone">
135
+ </p>
136
+
137
+ ## Being a good neighbor to the portal
138
+
139
+ Last Bell's traffic is built to look like what it is: **one parent, checking
140
+ the gradebook a few times a day** — never a crawler.
141
+
142
+ - **Eight polls a day by default** — one every 3 hours
143
+ (`LASTBELL_POLL_MINUTES=180`), and the interval is **clamped to a 15-minute
144
+ floor in code** ([`config.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/config.py)), so no misconfiguration
145
+ can hammer anyone's servers.
146
+ - **A poll is small, sequential, and identical to human use**: one login, one
147
+ home page, then per student the gradebook page, the class list, and one
148
+ fragment per class — the very `LoadControl` calls the portal's own UI issues
149
+ when you click through your classes, with a polite pause between them
150
+ ([`collector.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/collector.py)). A two-student, seven-class
151
+ household is ~21 requests per poll (~170/day) — fewer than a single manual
152
+ portal visit loads in page assets alone.
153
+ - **Zero portal traffic between polls.** Digests, summaries, and the dashboard
154
+ run entirely off the local database.
155
+ - **A failed poll just waits for the next cycle** ([`cli.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/cli.py))
156
+ — no retry storms.
157
+ - **It identifies itself honestly**: the User-Agent is `lastbell/<version>`
158
+ ([`client.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/client.py)), not a spoofed browser.
159
+
160
+ Portal terms vary by district and vendor and are yours to judge — but the
161
+ list above is the *entire* footprint, so you can judge it accurately.
162
+
163
+ ## Credentials & student data: the actual guarantees
164
+
165
+ **Your password touches exactly two things: your OS keyring and your
166
+ district's servers.** `lastbell set-password` stores it in the macOS
167
+ Keychain / Windows Credential Manager / Linux Secret Service
168
+ ([`secrets.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/secrets.py)); it never appears in `.env`, the
169
+ database, logs, or the source tree. (Docker installs inject it via
170
+ `LASTBELL_PASSWORD` from a secret store instead.) At runtime it is held in
171
+ memory and sent to one destination — your district's own login form, over
172
+ HTTPS enforced in [`config.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/config.py) — the same request your
173
+ browser makes ([`client.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/client.py)).
174
+
175
+ **Student data lives on your machine, full stop.** Snapshots, history, and
176
+ alerts sit in a local SQLite file in your user data dir (in a checkout,
177
+ `data/` — git-ignored, as is `.env`).
178
+ There is no telemetry, no analytics, no phone-home: the only outbound HTTP in
179
+ the codebase is the portal client ([`client.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/client.py)), the
180
+ district preflight ([`preflight.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/preflight.py)), and the alert
181
+ channels **you** configure ([`notify/`](https://github.com/noestudios/lastbell/blob/main/lastbell/notify)).
182
+
183
+ **What leaves is only what you route — and it's low-PII by design.** Alert
184
+ payloads carry initials + course + assignment, never a child's full name
185
+ ([`router.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/router.py)) — safe for a lock-screen preview. Know
186
+ your transports: `email` rides your own SMTP account; `ntfy` posts to the
187
+ public ntfy.sh unless you self-host, and **the topic name is the only
188
+ secret** there — make it long and random; `telegram` and `pushover` go
189
+ through those services' APIs.
190
+
191
+ **The dashboard shows full names, so it binds to `127.0.0.1` only** unless
192
+ you deliberately widen it — the bind address is the access control
193
+ ([`config.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/config.py), [`dashboard.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/dashboard.py)).
194
+
195
+ If the code ever stops backing one of these sentences, that's a bug — file it.
196
+
197
+ ## Why scraping (and not the SOAP API)
198
+
199
+ The legacy Edupoint SOAP mobile API is disabled on a growing number of districts
200
+ (MCPS returns `UPD5304-00`, Loudoun `D5517`). Last Bell talks to the PXP2 **web
201
+ portal** instead: an ASP.NET form login, then the `PXP2_Gradebook.aspx/LoadControl`
202
+ page method the gradebook UI itself calls. Run `lastbell preflight` to see what
203
+ your district allows.
204
+
205
+ ## Quickstart
206
+
207
+ Three commands, no files to edit. First get [pipx](https://pipx.pypa.io/stable/installation/)
208
+ (macOS: `brew install pipx`, Windows: `py -m pip install --user pipx`, Debian/Ubuntu:
209
+ `sudo apt install pipx`), then:
210
+
211
+ ```bash
212
+ pipx install lastbell
213
+ ```
214
+
215
+ ```bash
216
+ lastbell setup
217
+ ```
218
+
219
+ ```bash
220
+ lastbell run --loop
221
+ ```
222
+
223
+ `lastbell setup` is an interactive wizard: it confirms your district's portal
224
+ (MCPS offered as the default) before asking anything personal, puts your
225
+ password straight into the OS keyring, verifies login + data path + parsers
226
+ with the preflight, walks you through one notification channel (ntfy push /
227
+ email / SMS) ending in a live test message, and offers to run the first
228
+ collection. Re-run it any time — it remembers your answers. Settings land in
229
+ your user config dir, data in your user data dir (both printed at the end).
230
+
231
+ <details>
232
+ <summary>Running from a source checkout instead</summary>
233
+
234
+ ```bash
235
+ python3 -m venv .venv && source .venv/bin/activate
236
+ pip install -e .
237
+
238
+ cp .env.example .env # then edit: district + username (NOT the password)
239
+ lastbell set-password # stores the password in your OS keyring
240
+ lastbell preflight # district go/no-go check (values redacted)
241
+
242
+ lastbell run # one pass: snapshot, diff, alert (first run = baseline)
243
+ lastbell run --loop # keep polling every LASTBELL_POLL_MINUTES
244
+ lastbell collect # read-only JSON dump of what a run would persist
245
+ ```
246
+
247
+ A checkout's `.env` (in the working directory) takes precedence over the
248
+ installed settings file, and typically pins `LASTBELL_DB_PATH=data/lastbell.db`
249
+ to keep state inside the repo tree.
250
+ </details>
251
+
252
+ Then route alerts to the people who should get them (Phase 3):
253
+
254
+ ```bash
255
+ lastbell watcher add Mom --kind guardian --channel email=mom@example.com
256
+ lastbell watcher add Jasper --kind student --channel ntfy=some-long-secret-topic
257
+ lastbell subscribe Mom jasper # all alert types, all her channels
258
+ lastbell subscribe Jasper jasper \
259
+ --types assignment_missing,upcoming_deadline # students see nudges, not grades
260
+ lastbell subscriptions # who gets what
261
+ lastbell dashboard # web UI on 127.0.0.1:8321
262
+ ```
263
+
264
+ Want to see it populated before pointing it at your own kids? `lastbell
265
+ seed-demo` fabricates a fake family at end-of-quarter volume (two marking
266
+ periods, hundreds of assignments, months of history — no real student data)
267
+ and `lastbell dashboard --db <path it prints>` serves it.
268
+
269
+ Students are referenced by AGU or any unique name/initials prefix; watchers by
270
+ the name you gave them. You start with one automatically: the first `run`
271
+ creates a guardian watcher named after the credential holder, subscribed to
272
+ every student — email seeded from `LASTBELL_SMTP_TO` when set, console
273
+ otherwise. Its delivery follows the considerate default: one daily digest at
274
+ 4pm, with urgent alert types (missing assignment, upcoming deadline, grade
275
+ drop) sent immediately. Rename or remove it freely; it's only re-created if
276
+ the watcher list is ever empty again.
277
+
278
+ And shape *when and how much* each person hears (Phase 4):
279
+
280
+ ```bash
281
+ lastbell subscribe Mom jasper --at 17:00 # batch her alerts into a 5pm digest
282
+ lastbell subscribe Mom jasper --types daily_summary --at 07:00 # morning report
283
+ lastbell watcher quiet-hours Jasper 21:00-07:00 # held overnight, never dropped
284
+ lastbell alerts # the alert log
285
+ lastbell flush # send due digests/summaries now
286
+ ```
287
+
288
+ In `run --loop`, the portal is polled every `POLL_MINUTES` but the outbox and
289
+ summaries are checked **every minute**, so a 17:00 digest goes out at 17:00 —
290
+ not at the next three-hour poll. Time-based deliveries use the host's local
291
+ clock. An event subscribed both immediately and in a digest is sent once,
292
+ immediately. A summary reports *standing state* (overall marks, missing work,
293
+ what's due soon, the week's recent alerts); a digest batches the *events* that
294
+ fired.
295
+
296
+ ## Configuration & secrets
297
+
298
+ All non-secret settings live in one env file, written for you by `lastbell
299
+ setup` (in your user config dir; a checkout's git-ignored `.env` takes
300
+ precedence, with `.env.example` as its template). Passwords never go in that
301
+ file, the database, or the source tree — only a *reference* to where the
302
+ secret lives:
303
+
304
+ | Install | Secret store |
305
+ |----------------|---------------------------------------------------------------------|
306
+ | Bare-metal | OS keyring — macOS Keychain / Windows Credential Manager / Secret Service (`lastbell set-password`) |
307
+ | Docker / CI | `LASTBELL_PASSWORD`, injected from Docker secrets or a CI secret store |
308
+
309
+ Cross-platform by construction: plain Python (Windows/macOS/Linux), no OS-native
310
+ hooks. SQLite by default; ship it as a container to run identically on a Pi, NAS,
311
+ or VPS. It needs an **always-on host** to poll and push.
312
+
313
+ ## How alerts reach people
314
+
315
+ Push-**out**, not pull-in: nobody signs into Last Bell to receive an alert.
316
+ A *watcher* is just a name plus addresses; *subscriptions* say which student's
317
+ events reach them, over which channels, filtered by alert type. One poll, one
318
+ message per watcher-channel — a watcher subscribed to three alert types gets a
319
+ single message listing everything.
320
+
321
+ | Channel | Watcher address | Transport setup (env) |
322
+ |------------|------------------------|--------------------------------------------|
323
+ | `email` | `email=who@example.com`| `LASTBELL_SMTP_*` (any SMTP account) |
324
+ | *SMS* | carrier gateway addr, e.g. `email=3015551234@vtext.com` | same as email |
325
+ | `ntfy` | `ntfy=secret-topic` | none (public ntfy.sh) or `NTFY_SERVER/TOKEN` |
326
+ | `telegram` | `telegram=<chat_id>` | `LASTBELL_TELEGRAM_TOKEN` (@BotFather bot) |
327
+ | `pushover` | `pushover=<user_key>` | `LASTBELL_PUSHOVER_TOKEN` (app token) |
328
+ | `console` | — | none; prints to the run's stdout |
329
+
330
+ The web dashboard (`lastbell dashboard`) is for looking things up on
331
+ demand — students, assignments, alert log, grade history, watcher routing —
332
+ never required to get a notification. It's stdlib-only and binds `127.0.0.1`
333
+ unless you deliberately widen it; unlike alert payloads it shows full names,
334
+ so the bind address is the access control. Every page is a read; the only
335
+ writes are the watcher/subscription forms on /settings — household
336
+ bookkeeping, never grade data. Alert payloads stay **low-PII** (initials + course +
337
+ score, never a child's full name — safe for an SMS preview on a lock
338
+ screen).
339
+
340
+ ## Will it work for *my* district?
341
+
342
+ Probably, if your district runs the Synergy PXP2 web portal — and the
343
+ **preflight** (Phase 5) answers definitively, without installing anything else
344
+ or touching a `.env`:
345
+
346
+ ```bash
347
+ # Anonymous: public endpoints only, no credentials sent anywhere
348
+ lastbell preflight --district your-host.example --report
349
+
350
+ # Full: login + data path + this repo's actual parsers against your fragments
351
+ lastbell preflight --district your-host.example --username you --report
352
+ ```
353
+
354
+ It checks, in order: the PXP2 login form exists → the legacy SOAP API's status
355
+ (the deprecation code your district returns is kept verbatim — useful
356
+ cross-district data) → web login → students on the credential → the
357
+ `LoadControl` data path → and finally whether the **parsers understand your
358
+ district's fragments**, which is the question that actually decides
359
+ compatibility. Verdicts: `go`, `partial` (data path answers but a parser
360
+ needs a tweak — the most fixable kind of report), `no-go`, `anonymous-ok`.
361
+ Exit codes match (0 go, 1 not yet, 2 couldn't run) so it scripts cleanly.
362
+
363
+ `--report` prints Markdown that is **redacted by construction** — no student
364
+ names, grades, or usernames can appear in it — ready to paste into a
365
+ [district report issue](https://github.com/noestudios/lastbell/blob/main/.github/ISSUE_TEMPLATE/district-report.md). `--json`
366
+ is for scripts; `--show-values` reveals names locally only, and is never
367
+ included in exported output; `--dump` saves raw fragments to `debug/` in your
368
+ data dir (personal data — stays local, never committed) for parser
369
+ development. It also installs standalone as `parentvue-preflight`.
370
+
371
+ ## The Phase 0 gate — PASSED
372
+
373
+ The data path was reverse-engineered from the portal's own JavaScript and then
374
+ verified end-to-end against MCPS: `POST service/PXP2Communication.asmx/LoadControl`
375
+ with the page's verbatim `PXP.GBCurrentFocus` FocusArgs (and an `AGU` header)
376
+ returns server-rendered fragments; assignments arrive as a DevExpress grid
377
+ `dataSource` JSON array (`Date`, `GBAssignment`, `GBScore`, `GBPoints`, … with
378
+ LinkColumn cells wrapping display text and a ready-made
379
+ `Gradebook_AssignmentDetails` drill-down focus). The parsers in
380
+ [`lastbell/gradebook.py`](https://github.com/noestudios/lastbell/blob/main/lastbell/gradebook.py) are wired against
381
+ real captured fragments from both school types.
382
+
383
+ ```bash
384
+ lastbell preflight --dump # go/no-go check; saves raw fragments locally
385
+ lastbell collect # normalized JSON for every student and class
386
+ ```
387
+
388
+ Phase 1 built the watch loop on top of that path: each class row's `data-focus`
389
+ attribute carries the ready-made `{LoadParams, FocusArgs}` the portal's own
390
+ `GB.LoadControl` click handler sends, so `run` sweeps every class exactly the
391
+ way a human clicking through them would (with a polite delay between calls,
392
+ and duplicate screen/print row variants fetched once).
393
+
394
+ ## Roadmap
395
+
396
+ | Phase | What ships |
397
+ |------:|------------|
398
+ | **0** | ✅ Pass the gate; harden the connector into normalized courses + assignments |
399
+ | **1** | ✅ All-class sweep, persisted snapshots (keyed on the Edupoint assignment GUID), diff + first alert (`run` / `run --loop`) |
400
+ | **2** | ✅ Missing, ungraded-past-due, future-deadline look-ahead, score changes (`LOOKAHEAD_DAYS` / `UNGRADED_GRACE_DAYS`) |
401
+ | **3** | ✅ Watcher accounts (guardians & students), subscriptions, dashboard, channels |
402
+ | **4** | ✅ Daily student summaries, digests, quiet hours, grade-drop thresholds |
403
+ | **5** | ✅ Publish the preflight as a redacted, general district tool |
404
+
405
+ ## Credits
406
+
407
+ The dashboard's visual design (colors, type, card and badge styling in
408
+ [`lastbell/style.css`](https://github.com/noestudios/lastbell/blob/main/lastbell/style.css)) is derived from
409
+ [Purity UI Dashboard](https://github.com/creativetimofficial/purity-ui-dashboard)
410
+ — Copyright (c) 2021 Creative Tim, released under the MIT license; its
411
+ copyright and permission notice applies to those derived styles.
412
+
413
+ The web-portal approach (ASP.NET form login, embedded child-list JSON) was
414
+ first demonstrated by [dmc5179/ParentVUE](https://github.com/dmc5179/ParentVUE)
415
+ (GPLv3), which served as prior art and reference during this project's district
416
+ recon. Last Bell's code is written independently against the portal itself, but
417
+ that repo deserves the credit for proving the post-SOAP path first. Community
418
+ documentation of the (now largely deprecated) SOAP API lives at
419
+ [StudentVue/docs](https://github.com/StudentVue/docs).
420
+
421
+ ## License
422
+
423
+ MIT — see [LICENSE](https://github.com/noestudios/lastbell/blob/main/LICENSE). Because the connector is original code (not a fork),
424
+ the license is a free choice; MIT is the permissive default for maximum forkability.