From bf2e5421827d239b0804ce21b17b47c4e147a8db Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 12 Sep 2026 16:39:50 +0200 Subject: [PATCH] Add keepalive-status, a log-only session health check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summarises the running container's keepalive: extension count, latest budget, transient failures, rejections, restarts. It deliberately makes no API call. Any authenticated request slides the session TTL, so a checker that talked to Schulcloud would be sustaining the session itself and could not report on whether the keepalive is doing it — the same confound that made the first endurance test ambiguous. Reading container logs observes without participating. Warns when the reported budget drops below 7000s, which is the early signal that extensions have stopped taking effect. Co-Authored-By: Claude Opus 5 --- README.md | 1 + docs/AUTH.md | 11 ++++++-- docs/DEPLOYMENT.md | 3 +- package.json | 3 +- scripts/keepalive-status.sh | 56 +++++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100755 scripts/keepalive-status.sh diff --git a/README.md b/README.md index 3a2d419..61ca239 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ npm test # unit tests, no network npm run probe # check assumptions against the live instance npm run smoke # full end-to-end: real server, real client, real data npm run session-diagnose # instrument what actually ends the session (~2.5h) +npm run keepalive-status # is the deployed container holding its session? npm run typecheck ``` diff --git a/docs/AUTH.md b/docs/AUTH.md index 489ebd8..f776fbc 100644 --- a/docs/AUTH.md +++ b/docs/AUTH.md @@ -113,8 +113,15 @@ permanently and says what to do, because a deleted whitelist entry cannot be revived by retrying — and if it happens roughly two hours after login, suspect an open tab before anything else. -`npm run session-diagnose` logs the budget every 10 minutes for ~2.5 h, which is -the direct way to confirm a token is holding. +Two ways to check that a token is holding: + +- `npm run keepalive-status` reads the running container's logs and summarises + its extensions. It makes **no** API call on purpose — any authenticated + request slides the TTL, so a checker that talked to Schulcloud would be + keeping the session alive itself and could not tell you whether the keepalive + works. +- `npm run session-diagnose` calls refresh-session every 10 minutes for ~2.5 h + and logs the budget, for when there is no container to read logs from. **Operational consequence:** if the container is down for over two hours the session lapses on its own, and restarting will not recover it. The startup log diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 3df6c91..32f3b92 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -157,7 +157,8 @@ npm run probe # re-verify the API assumptions `MAX_DOWNLOAD_BYTES` (25 MiB default). - **The Schulcloud session has a 2-hour sliding TTL**, so the server calls `refresh-session` every 30 minutes. Watch for - `keepalive: session extended, 7200s` in the logs. + `keepalive: session extended, 7200s` in the logs, or run + `npm run keepalive-status` for a summary. - **Never leave a Schulportal tab open on the token you deployed.** It shares the session and its auto-logout will revoke it ~2h after login. Copy the cookie in a private window and close it — see docs/AUTH.md. diff --git a/package.json b/package.json index a61e2e2..5f5751a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "test": "node --test test/*.test.ts", "probe": "node --env-file=.env scripts/probe.mjs", "smoke": "node --env-file=.env scripts/smoke.mjs", - "session-diagnose": "node --env-file=.env scripts/session-diagnose.mjs" + "session-diagnose": "node --env-file=.env scripts/session-diagnose.mjs", + "keepalive-status": "bash scripts/keepalive-status.sh" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.20.0", diff --git a/scripts/keepalive-status.sh b/scripts/keepalive-status.sh new file mode 100755 index 0000000..db7bcc6 --- /dev/null +++ b/scripts/keepalive-status.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Reports on the running container's keepalive from its logs alone. +# +# Deliberately makes no API call: any authenticated request slides the session +# TTL, so a checker that talked to Schulcloud would be keeping the session +# alive itself and could not tell you whether the keepalive is working. +set -uo pipefail + +CONTAINER="${1:-schulcloud-mcp}" + +if ! docker inspect "$CONTAINER" >/dev/null 2>&1; then + echo "container '$CONTAINER' does not exist" + exit 1 +fi + +state=$(docker inspect -f '{{.State.Status}}' "$CONTAINER") +health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CONTAINER") +started=$(docker inspect -f '{{.State.StartedAt}}' "$CONTAINER") +restarts=$(docker inspect -f '{{.RestartCount}}' "$CONTAINER") + +echo "container : $CONTAINER state=$state health=$health restarts=$restarts" +echo "started : $started" + +logs=$(docker logs -t "$CONTAINER" 2>&1) + +ok=$(grep -c 'session extended' <<<"$logs" || true) +fail=$(grep -c 'token rejected (401)' <<<"$logs" || true) +retry=$(grep -c 'ping failed' <<<"$logs" || true) + +echo "keepalive : ${ok} successful extension(s), ${retry} transient failure(s), ${fail} rejection(s)" + +first=$(grep 'session extended' <<<"$logs" | head -1 | awk '{print $1}') +last_line=$(grep 'session extended' <<<"$logs" | tail -1) +[ -n "$first" ] && echo "first ok : $first" +if [ -n "$last_line" ]; then + echo "latest ok : $(awk '{print $1}' <<<"$last_line")" + echo " $(sed 's/^[^ ]* *//' <<<"$last_line")" + # A budget below the instance's 7200s ceiling means extensions are not taking. + budget=$(grep -oE 'extended, [0-9]+s' <<<"$last_line" | grep -oE '[0-9]+') + if [ -n "$budget" ] && [ "$budget" -lt 7000 ]; then + echo "WARNING : budget ${budget}s is below the 7200s ceiling — extensions may not be taking effect" + fi +fi + +if [ "$fail" -gt 0 ]; then + echo + echo "SESSION LOST:" + grep 'token rejected (401)' <<<"$logs" | tail -2 + echo "=> paste a fresh jwt cookie into .env and restart (docs/AUTH.md)." + exit 2 +fi + +if [ "$ok" -gt 0 ]; then + echo + echo "OK — session held across ${ok} extension(s) with no rejection." +fi