Read the user's own lesson notes, and the class register behind them
Schulcloud says what was uploaded and WebUntis says what was scheduled. Neither says what was *taught* — which point the teacher laboured, which example landed, what "will definitely come up". That lives in two places this server could not reach: the notes the user takes in the lesson, and WebUntis' class register. Notes are a directory of Markdown files (NOTES_DIR), not a table. They have to be writable from a phone in a classroom, readable when Postgres is down, and outlive this project, and files are the only shape that is all three — so the files are the truth and the index is a view of them, the same split as file_texts and the mirror. list_notes and get_note read disk, so they answer before the first crawl; search, what_changed and all three German prompts read them alongside the Schulcloud material. add_note writes one, and is the only thing in this server that writes anything. That is not a hole in the read-only invariant but a different store: it is bounded to NOTES_DIR by the same safeComponent/resolveWithin pair that stops a hostile Schulcloud filename escaping the mirror, so a note titled ../../.ssh/authorized_keys becomes a filename. Schulcloud and WebUntis stay GET-only and allowlisted respectively. NOTES_READONLY refuses writes outright. Appending targets the *lesson*, not the title: "halt das auch noch fest" mid-lesson carries a new title, and deriving the path from it would start a second note every time, which is the one thing append exists to prevent. Notes.app has no export — its bodies are compressed protobuf and the iCloud copy is encrypted — so scripting the app is not the clumsy route to the notes but the only one. scripts/export-apple-notes.js reads them through AppleScript into one JSON object per line, and `schulcloud note import` converts the HTML to Markdown, takes the Notes folder as the subject and the *creation* date as the lesson's date. Attachments cannot come across; a note that was a photo of the board imports as a line saying so, because importing it empty would hide the loss. The class register needed one API property to become cheap: getLessonTopic2017 answers per *series*, not per period, so a term is reconstructed by asking about the latest period of each lesson series and merging back by id — a few dozen calls for a school year rather than one per lesson. untis_lesson_topics now takes a subject as well as a period id, and UNTIS_HISTORY_DAYS of register goes into the index under a kind of its own, so "what did we actually do before the test" is searchable. Sharing the snapshot rather than duplicating it caught one thing on the way: the search tool's live path had to learn notes too, or fresh=true would have quietly disagreed with the index. 305 tests; 88/89 smoke against the local instance, the one failure being the H5P service that instance does not run. The live smoke could not be retaken: that session has lapsed and needs a fresh jwt cookie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,25 @@ export interface Config {
|
||||
/** How often to re-crawl on a timer. Zero = only on demand. */
|
||||
crawlIntervalMs: number;
|
||||
|
||||
/**
|
||||
* Where the user's own lesson notes live, as Markdown files. Unset = the
|
||||
* note tools are not offered, the same rule the untis_* tools follow.
|
||||
*/
|
||||
notesDir: string | undefined;
|
||||
/**
|
||||
* Whether add_note may write. The notes directory is the only thing in this
|
||||
* server anything can write to, so turning it off is a real setting and not
|
||||
* a theoretical one — a deployment that syncs its notes in from elsewhere
|
||||
* wants the files left alone.
|
||||
*/
|
||||
notesWritable: boolean;
|
||||
/**
|
||||
* How far back to read the WebUntis class register into the index. Zero =
|
||||
* not at all. Costs one timetable call per 90 days plus one per lesson
|
||||
* series, so a school year is a few dozen requests on a background crawl.
|
||||
*/
|
||||
untisHistoryDays: number;
|
||||
|
||||
/**
|
||||
* WebUntis, where the school keeps the timetable. Unset = the untis_* tools
|
||||
* are not offered at all, which is the right answer for a school that does
|
||||
@@ -210,6 +229,11 @@ export function loadConfig(): Config {
|
||||
// so an index without it misses whole courses. One page load per folder.
|
||||
indexFileManager: bool('INDEX_FILE_MANAGER', true),
|
||||
crawlIntervalMs: intAllowingZero('CRAWL_INTERVAL_MS', 6 * 60 * 60_000),
|
||||
// Absolute for the same reason as the mirror: resolveWithin only returns
|
||||
// an absolute path when the root it is given is one.
|
||||
notesDir: process.env.NOTES_DIR?.trim() ? resolve(process.env.NOTES_DIR.trim()) : undefined,
|
||||
notesWritable: !bool('NOTES_READONLY', false),
|
||||
untisHistoryDays: intAllowingZero('UNTIS_HISTORY_DAYS', 180),
|
||||
untis: untisConfig(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user