Commit Graph

3 Commits

Author SHA1 Message Date
a8badc2fd1 Read submitted text and teacher feedback from the homework page
You were right that this data never reaches the browser as an API call.
The legacy front end calls the Feathers API server-side for
submission.comment, submission.grade and submission.gradeComment and
renders them into GET /homework/{taskId}. That Feathers API is not
exposed publicly — /api/v1/* 404s — so the rendered page is the only way
to reach these fields from outside.

core/homework-page.ts parses it, hooked on the data-testid attributes
the project's own e2e tests use rather than incidental markup. The page
authenticates by jwt *cookie*; an Authorization header is ignored and
redirects to the identity provider. Every field is optional and parse
failures return undefined, so a markup change degrades to "not found"
and cannot break get_task. The wording distinguishes the two: absent
feedback is reported as not found, never as none given.

Measured on one course: 4 of 7 graded submissions carry feedback no API
call can return — "vollständig und nachvollziehbar", "Feedback siehe
Zettel", and so on.

This exposed a bug in a shared utility: htmlToText decoded only six
entities, so any named entity passed through raw. German content makes
that routine — "vollständig" would have reached the model verbatim
from boards and task descriptions too, not just here. It now decodes
named, decimal and hex references in one pass, so ä stays
literal instead of decoding twice, and leaves unknown names alone rather
than mangling them.

Also fixes a documented-recovery bug found while restoring the session:
`docker compose restart` does not re-read env_file, so it silently kept
serving the dead token. `up -d` is correct and the docs said the wrong
thing.

78 tests, 38/38 smoke; verified end to end through Claude Code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 13:25:35 +02:00
359c46afad Add the schulcloud CLI, and document the split
The CLI talks only to the Pi's /api surface and holds no Schulcloud
credential — only the same bearer token the Claude connector uses. That
is not layering for its own sake: a Schulcloud session dies after two
hours idle and a CLI process lives for seconds, so a CLI with its own
token would be dead most times you reached for it. Routing through the
Pi means one session, one keepalive, one monthly cookie paste.

sync is a one-way mirror, which follows from the data rather than from
scope-cutting: file records are immutable upstream, so there is no
versioning, no conflict resolution and no merge. State is keyed by file
record id with the path as derived output, so an upstream rename moves
the local file instead of duplicating it — verified against the live
server. Verification is size-only because the download endpoint exposes
no ETag and Schulcloud publishes no hash; size still catches the failure
that happens, a truncated download. Downloads land on a .part neighbour
and are renamed, so an interrupted run leaves no half-file that a later
run mistakes for complete. Deletions are reported but not propagated —
a teacher removing a worksheet is no reason to destroy the student's
copy — with --prune to opt in.

what_changed now clamps to the oldest stored generation instead of
refusing, and says it did: "what's new this week" is a reasonable
question to ask a two-day-old index.

Two build bugs caught by the checks rather than by luck: the smoke
harness constructed the app without services, so the index-backed tools
were never exercised; and the Docker build could not see
scripts/copy-assets.mjs, so the image would have shipped without
migrations and silently degraded to live-only.

67 unit tests (9 needing Postgres), smoke green both ways — 34 checks
with an index, 32 without, because graceful degradation is a supported
mode and not a fallback nobody runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 21:26:33 +02:00
c9bcd3de31 Record extension options and decisions in docs/ROADMAP.md
Nothing implemented. Captures the setup decisions (bearer token,
stateless, inline extraction) and, for any future persistence, that it
goes in the Pi's existing PostgreSQL under its own database and user.

Worth keeping because the build surfaced facts that are expensive to
rediscover: search covers file names but never file contents, so the 93
PDFs here are opaque to it; one search costs ~270 upstream requests;
extracted file text is immutable per fileRecord and so cacheable
forever; German needs the german FTS dictionary plus pg_trgm, since
compounds defeat stemming; and "what's new since X" is impossible today
because the API has no changed-since filter anywhere.

Also records what is ruled out and why — collaborative text editor
contents are not retrievable, OCR is unnecessary since images go to
Claude directly, and write tools would forfeit the read-only property
that makes the public endpoint acceptable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 17:12:52 +02:00