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.
- lastbell-0.1.0/LICENSE +21 -0
- lastbell-0.1.0/PKG-INFO +424 -0
- lastbell-0.1.0/README.md +388 -0
- lastbell-0.1.0/lastbell/__init__.py +7 -0
- lastbell-0.1.0/lastbell/app.js +439 -0
- lastbell-0.1.0/lastbell/cli.py +649 -0
- lastbell-0.1.0/lastbell/client.py +235 -0
- lastbell-0.1.0/lastbell/collector.py +144 -0
- lastbell-0.1.0/lastbell/config.py +106 -0
- lastbell-0.1.0/lastbell/dashboard.py +2090 -0
- lastbell-0.1.0/lastbell/differ.py +212 -0
- lastbell-0.1.0/lastbell/favicon.png +0 -0
- lastbell-0.1.0/lastbell/gradebook.py +338 -0
- lastbell-0.1.0/lastbell/mcps_schools.json +2116 -0
- lastbell-0.1.0/lastbell/models.py +112 -0
- lastbell-0.1.0/lastbell/notify/__init__.py +125 -0
- lastbell-0.1.0/lastbell/notify/console.py +7 -0
- lastbell-0.1.0/lastbell/notify/email.py +93 -0
- lastbell-0.1.0/lastbell/notify/ntfy.py +45 -0
- lastbell-0.1.0/lastbell/notify/pushover.py +39 -0
- lastbell-0.1.0/lastbell/notify/telegram.py +39 -0
- lastbell-0.1.0/lastbell/outbox.py +178 -0
- lastbell-0.1.0/lastbell/paths.py +77 -0
- lastbell-0.1.0/lastbell/preflight.py +429 -0
- lastbell-0.1.0/lastbell/router.py +133 -0
- lastbell-0.1.0/lastbell/schema.sql +125 -0
- lastbell-0.1.0/lastbell/schools.py +138 -0
- lastbell-0.1.0/lastbell/secrets.py +83 -0
- lastbell-0.1.0/lastbell/seed.py +264 -0
- lastbell-0.1.0/lastbell/setup_wizard.py +397 -0
- lastbell-0.1.0/lastbell/store.py +236 -0
- lastbell-0.1.0/lastbell/style.css +1099 -0
- lastbell-0.1.0/lastbell/summary.py +159 -0
- lastbell-0.1.0/lastbell/watchers.py +400 -0
- lastbell-0.1.0/lastbell.egg-info/PKG-INFO +424 -0
- lastbell-0.1.0/lastbell.egg-info/SOURCES.txt +58 -0
- lastbell-0.1.0/lastbell.egg-info/dependency_links.txt +1 -0
- lastbell-0.1.0/lastbell.egg-info/entry_points.txt +3 -0
- lastbell-0.1.0/lastbell.egg-info/requires.txt +9 -0
- lastbell-0.1.0/lastbell.egg-info/top_level.txt +1 -0
- lastbell-0.1.0/pyproject.toml +58 -0
- lastbell-0.1.0/setup.cfg +4 -0
- lastbell-0.1.0/tests/test_config.py +80 -0
- lastbell-0.1.0/tests/test_dashboard.py +1109 -0
- lastbell-0.1.0/tests/test_differ.py +97 -0
- lastbell-0.1.0/tests/test_gradebook.py +75 -0
- lastbell-0.1.0/tests/test_outbox.py +154 -0
- lastbell-0.1.0/tests/test_paths.py +53 -0
- lastbell-0.1.0/tests/test_phase4.py +159 -0
- lastbell-0.1.0/tests/test_preflight.py +208 -0
- lastbell-0.1.0/tests/test_refresh_schools.py +68 -0
- lastbell-0.1.0/tests/test_rollover.py +153 -0
- lastbell-0.1.0/tests/test_router.py +135 -0
- lastbell-0.1.0/tests/test_schools.py +87 -0
- lastbell-0.1.0/tests/test_seed.py +89 -0
- lastbell-0.1.0/tests/test_setup.py +237 -0
- lastbell-0.1.0/tests/test_store.py +123 -0
- lastbell-0.1.0/tests/test_summary.py +105 -0
- lastbell-0.1.0/tests/test_time_rules.py +129 -0
- 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.
|
lastbell-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+
|
|
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.
|