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