Document WebUntis
docs/API.md gains the findings: the endpoint and its required version parameter, the one-time code and the two error codes worth naming, the Z that means local time, a substitution being two periods, the unused exam module that puts announced tests in the period notes, and a day without lessons that is not a holiday. Those cost an afternoon of probing to learn and nothing upstream states them. docs/AUTH.md sets the key against the Schulcloud token it sits beside: no password, nothing to keep alive, revocable on its own, and not read-only in itself — which is why the allowlist exists. PI.md and DEPLOYMENT.md add the four values, the container recreate a changed key needs, and the clock requirement; the Pi's troubleshooting table gains both failure messages. README and CLAUDE.md say what the server now is: Schulcloud for the material, WebUntis for the day. Smoke is 89 checks with the index and a key, 87 live-only, 9 fewer without one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
69
docs/API.md
69
docs/API.md
@@ -288,6 +288,75 @@ Two more, seen while building the local fixture:
|
||||
me" query reads `refPermModel`, so a share made through it never appears
|
||||
under Geteilte Dateien. The share-link flow patches `/files/{id}` directly.
|
||||
|
||||
## WebUntis is a fourth store, and holds the timetable
|
||||
|
||||
Schulcloud's `times` on `/api/v1/courses` are empty for this school, so nothing
|
||||
in Schulcloud says when a lesson happens, let alone that it was cancelled. That
|
||||
lives in **WebUntis**, a separate product with its own login. What this server
|
||||
uses is the API the Untis Mobile app uses, verified against
|
||||
`ags-erfurt.webuntis.com` on 2026-09-17.
|
||||
|
||||
- **One endpoint, JSON-RPC:** `POST /WebUntis/jsonrpc_intern.do?m=<method>&school=<school>&v=i3.2`,
|
||||
with `{"jsonrpc":"2.0","method":<method>,"params":[{…, "auth":{…}}]}`.
|
||||
- **`v` is not optional.** Omit it and the call fails with `-8998` carrying a
|
||||
Java `NullPointerException` from `getParameter`, which reads like a bug in
|
||||
the request body and is not.
|
||||
- Errors arrive with **HTTP 200** and an `error` member, so the body has to be
|
||||
checked before the status.
|
||||
- A plain `user-agent: schulcloud-mcp` is accepted; there is no need to
|
||||
impersonate the app.
|
||||
- **Authentication is a TOTP over a static base32 key**, no password and no
|
||||
session: `auth: { user, otp, clientTime }` on every call, where `otp` is the
|
||||
6-digit RFC 6238 code for the key from Profil → Freigaben → Untis Mobile.
|
||||
- **Send the code as a string.** One in ten starts with a zero, which a JSON
|
||||
number silently drops. Both shapes are accepted, so only the string is
|
||||
always right.
|
||||
- `-8504 bad credentials` = wrong key or user. `-8524 invalid client time` =
|
||||
the host's clock is off, which is its own failure and worth naming.
|
||||
- The response sets a `JSESSIONID`, but nothing needs it: each request
|
||||
authenticates itself, which is why there is no keepalive on this side.
|
||||
- **Methods that exist** (and are all this server may call — see the allowlist
|
||||
in `core/untis.ts`): `getUserData2017`, `getTimetable2017`,
|
||||
`getLessonTopic2017`, `getHomeWork2017`, `getMessagesOfDay2017`.
|
||||
`getClassregEvents2017` and `getSchoolyears2017` answer "Method not found";
|
||||
`getPeriodData2017` answers with empty objects for a student.
|
||||
- **`startDateTime` claims to be UTC and is not.** Lessons come back as
|
||||
`2026-09-21T08:00Z` and the school's time grid starts at 08:00 local, so the
|
||||
`Z` is decoration. `new Date(...)` would move every lesson by an hour or two,
|
||||
twice a year by a different amount; `splitLocal` takes the string apart
|
||||
instead.
|
||||
- **A substitution is two periods, not one changed period.** The original turns
|
||||
up with `is: ["CANCELLED"]` and the replacement beside it with
|
||||
`is: ["IRREGULAR"]`, same slot, different teacher. `orgId` on an element also
|
||||
exists (the room moves that way), so both have to be read. Statuses seen on a
|
||||
real account: `REGULAR`, `CANCELLED`, `IRREGULAR`.
|
||||
- **`getTimetable2017` carries the whole master data with every answer** —
|
||||
subjects, teachers with full names, rooms, classes, every holiday of six
|
||||
school years. There is a `masterDataTimestamp` delta protocol; its removal
|
||||
semantics are unverified, so this server asks for the full set and caches it
|
||||
for a few hours.
|
||||
- **Homework hangs off the periods too.** `getHomeWork2017` filters by the
|
||||
homework's own dates, so a window ending today shows nothing due tomorrow and
|
||||
nothing set last month — and the same items appear inline on the timetable's
|
||||
periods, where they need no second call.
|
||||
- **`getLessonTopic2017` takes `periodId`, singular.** It answers with
|
||||
`previousTopics`: what the earlier lessons of that series actually covered,
|
||||
from the class register. A `periodIds` array is rejected as "period 0 not
|
||||
found".
|
||||
- **The exam module is unused at this school**, so `getExams2017` is empty and
|
||||
`period.exam` is null. Announced tests are typed into the period's **info
|
||||
text** instead ("LF10: Leistungskontrolle agile Softwareentwicklung …"), which
|
||||
makes `text.info` the most valuable field in the payload rather than a
|
||||
footnote.
|
||||
- **A day with no lessons is not necessarily a holiday.** At a vocational school
|
||||
the weeks spent in the company simply have no periods, and `holidays` says
|
||||
nothing about them. Reporting "Ferien" there would be wrong; so would an
|
||||
empty answer.
|
||||
- **The key can write.** This account's `rights` are `CLASSREGISTER`,
|
||||
`R_MY_ABSENCES`, `W_OWN_ABSENCE`, `R_OFFICEHOURS` — the mobile API can report
|
||||
an absence for the user. Nothing here does, and the allowlist is what
|
||||
guarantees it.
|
||||
|
||||
## Re-verifying after an upstream release
|
||||
|
||||
`npm run probe` re-checks every assumption above against the live instance and
|
||||
|
||||
Reference in New Issue
Block a user