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>
52 lines
2.5 KiB
Bash
Executable File
52 lines
2.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Mint a session on the local instance and print the environment the MCP server
|
|
# and CLI expect, so they can be pointed at it instead of the live Schulcloud.
|
|
#
|
|
# eval "$(./scripts/mcp-env.sh)" # as the demo student
|
|
# eval "$(./scripts/mcp-env.sh klara.fall@schul-cloud.org Schulcloud1\!)"
|
|
#
|
|
# Nothing is written to the repo: the output contains a live session token, and
|
|
# a throwaway instance is still no reason to start committing those.
|
|
set -euo pipefail
|
|
|
|
# `localhost`, not `127.0.0.1`: it must match SC_DOMAIN, because urls the server
|
|
# hands back (Etherpad pads, for one) are built from it, and the client refuses
|
|
# to follow a url onto a different host rather than leak a session cookie there.
|
|
URL=${LOCAL_SC_URL:-http://localhost:4400}
|
|
USER=${1:-demo-schueler@schul-cloud.org}
|
|
PASS=${2:-schulcloud}
|
|
|
|
token=$(curl -fsS -X POST "$URL/api/v3/authentication/local" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "$(printf '{"username":%s,"password":%s}' \
|
|
"$(printf '%s' "$USER" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')" \
|
|
"$(printf '%s' "$PASS" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')")" \
|
|
| python3 -c 'import json,sys; print(json.load(sys.stdin)["accessToken"])')
|
|
|
|
# The index and mirror are pinned too, not just the instance. The repo's .env
|
|
# normally points at the live account, and process env beats --env-file, so
|
|
# without these a local smoke run would crawl this throwaway instance straight
|
|
# into the live index — and a per-course refresh carries everything outside its
|
|
# scope forward, so the fixtures would outlive the run. Fixtures in real data
|
|
# is exactly the accident the store tests' "test" guard exists to prevent.
|
|
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
|
|
|
cat <<ENV
|
|
export TSC_URL=$URL
|
|
export TSC_JWT_COOKIE=$token
|
|
export MCP_AUTH_TOKEN=local-instance-token
|
|
export DATABASE_URL=postgresql://schulcloud:schulcloud@127.0.0.1:55432/schulcloud_local
|
|
export MIRROR_DIR=$ROOT/tmp/mirror-local
|
|
# Notes are pinned for the same reason as the mirror: they are the one thing
|
|
# this server writes, and a local run has no business writing into the real ones.
|
|
export NOTES_DIR=$ROOT/tmp/notes-local
|
|
export INDEX_PERSONAL_FILES=true
|
|
# WebUntis off for a local run: this instance has no timetable, and the key in
|
|
# the repo's .env belongs to the real school — a fixture run has no business
|
|
# talking to it, even read-only.
|
|
export UNTIS_SERVER=
|
|
export UNTIS_SCHOOL=
|
|
export UNTIS_USER=
|
|
export UNTIS_SECRET=
|
|
ENV
|