Close the gaps an audit of courses, tasks, files and grades turned up

Every area — courses, rooms, boards, topics, tasks, files, quizzes, teams,
groups, submissions, grades — was checked for data the instance has and the
tools did not show.

Grades and feedback. A teacher's /homework page is a different page from a
student's: grade and comment live in the grading form, one block per
submission, so a teacher account reported every graded submission as having
neither. parseTeacherGrading reads the form, and list_submissions can now
include the written feedback and who handed the work in.

Names. /api/v1 is partly served: courses, users and classes survive in the
deployment's ingress table, and users/{id} is the only route from an id to a
name. Submitters, file creators and course teachers resolve through it, and
degrade to "not visible to this account" where a student may not read them.

Courses, rooms and classes. get_course adds the description, teachers,
member count and weekly timetable from /api/v1/courses. list_classes is new.
get_room reports what the account may do — allowedOperations is an object of
booleans, not the list it was typed as — and applicants and invitation links
where it may manage them.

Board and topic content. Link descriptions, image alt text, drawing and
video-conference titles, the ids behind external tools and H5P content (the
only thing resembling a quiz), and what a deleted element used to be. Topic
Etherpad pads are read like board pads, and htmlToText keeps table columns
apart and drops template indentation.

Files. A scan with no text layer falls back to the preview endpoint, whose
width and outputFormat are undocumented enums, so Claude gets a picture of
the page; list_files reports counts and sizes. Teams stay documented as
unreadable at any API version; their files come later.

What the crawl missed. Tasks attached to topics (18 of 60 on the live
account), each course's own file area, and — behind INDEX_PERSONAL_FILES —
personal files and submissions with their grade comments, so search and
what_changed cover grading. A submission hit points at get_task.

The local instance's preview profile gets an ImageMagick policy that allows
the coders its 7.1.2 build needs; the image's own denies them all.

110 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-09-16 20:19:16 +02:00
parent a3b17a680c
commit 5ae2210459
25 changed files with 1462 additions and 89 deletions

View File

@@ -70,7 +70,12 @@ bin/cli.ts ──HTTP──────────┘ cli/{config,client,sync
- **`store/`** — crawl generations, identity diffs, `german` + `pg_trgm` FTS.
`Store.open` returns `undefined` when Postgres is down; callers degrade.
- **`indexer/`** — crawl → persist → mirror bytes → extract text → index.
Coalesces concurrent refreshes; enforces a minimum interval.
Coalesces concurrent refreshes; enforces a minimum interval. The crawl walks
topic-attached tasks too, which the course page does not list: without that
they are unsearchable and their grades invisible. `INDEX_PERSONAL_FILES`
additionally indexes personal files and submitted/returned work, including
grade comments — that is what makes "what got graded this week" answerable,
at roughly three extra requests per task.
- **`mcp/tools/*.ts`** — tool descriptions are prompts: they are how Claude picks
a tool, so they carry the German domain terms (Kurse, Themen, Aufgaben) and say
when *not* to use the tool.
@@ -146,8 +151,25 @@ These cost real time to discover; `docs/API.md` has the full list with evidence.
so the store's digest has to include the name.
- **Submissions: only `GET /submissions/status/task/{taskId}` exists.** No list,
no fetch-by-id, and the payload has no submitted text, grade comment or
graded-at`/api/v1`, which had them, is not served here. Don't imply absent
feedback means none was given.
graded-at. Don't imply absent feedback means none was given.
- **`/api/v1` is partly served, and it is production surface.** Exactly three
legacy routes survive in the deployment's own ingress table
(`dof_app_deploy/ansible/group_vars/all/x_ingress.yml`): **`/api/v1/courses`,
`/api/v1/users`, `/api/v1/classes`**. Everything else under `/api/v1` is
unrouted and 404s. They matter because v3 dropped things they still carry:
`courses` has the description, `teacherIds`, `userIds` and `times` (the weekly
timetable), and `users/{id}` is the **only** way to turn a user id into a name
— submission `submitters`, file `creatorId` and course `teacherIds` are
otherwise unreadable. Permission is per-account: a teacher may read their
students, a student may read only themselves, so name resolution must degrade
to "not visible to this account" rather than printing a bare id.
- **The teacher's homework page is a different page from the student's.** Its
tabs are `extended` and `submissions`, not `submission` and `feedback`, and
the grade lives in the grading *form* (`name="grade"`, `name="gradeComment"`,
one block per `submissionId`) rather than in rendered prose. The student
parser finds nothing on it, which is why a teacher account reported every
graded submission as "neither a percentage nor feedback was found" while the
data was plainly there. `parseTeacherGrading` handles that side.
- **A grade is a percentage (`Number` 0-100) or absent; there is no text grade.**
Teachers commonly grade with `gradeComment` alone, so "graded by feedback" is
a complete answer. `formatGradeState` in `mcp/tools/submissions.ts` owns that
@@ -174,7 +196,27 @@ These cost real time to discover; `docs/API.md` has the full list with evidence.
dedicated endpoints (`/boards/{id}`, `/cards`, file records) are stable.
- Many course PDFs are **image-only scans with no text layer** (3 of 4 sampled),
so extraction legitimately yields nothing. `extract.ts` detects this and says
so; do not "fix" it by retrying.
so; do not "fix" it by retrying. `download_file` then falls back to
`GET /file/preview/...`, which renders the page as a picture Claude can read —
the answer for a scan, though it still leaves the file unsearchable.
- **The preview endpoint has two enums, and both 400 without saying so.**
`width` accepts only **50, 150 or 500** — a number outside that set is a
validation error naming the value but not the permitted set. `outputFormat`
accepts only **`image/webp`**; omitting it is worse than wrong, because the
preview is then rendered in the *source* format and a PDF comes back as a
PDF. The response also labels itself `webp` rather than `image/webp`, so the
content type has to be normalised before anything will treat it as an image.
- **A room's `allowedOperations` is an object, not a list.** Every operation is
present with a boolean; `false` means denied. Typing it as `string[]`
type-checks and throws `.some is not a function` the moment anything reads it.
- **Schulcloud has no quiz of its own.** There is no quiz module or endpoint
upstream: interactive exercises are H5P elements, whose `contentId` is the
only handle onto the content, or external (LTI) tools behind
`contextExternalToolId`. Say that rather than looking for a quiz API.
- **Teams cannot be read at any version.** v3 exposes only
`GET /team/{teamId}/news`; upstream `main`'s teams controller is write-only
(`POST :teamId/create-room`). `/teams` is the legacy client's HTML page, not
an API. This one is genuinely unavailable, not merely uncovered.
- **`exp` (30 days) is not the session lifetime.** The binding limit is a Valkey
whitelist entry with a `JWT_TIMEOUT_SECONDS` TTL (7200s; live value at
`GET /api/v3/config/public`) that every authenticated request re-sets.