T11: Grades provider (courses, course detail) #10

Merged
dzenda merged 81 commits from t11-grades into t10-sirius 2026-11-14 17:50:24 +01:00
Owner

T11: Grades provider (fit.grades.courses, fit.grades.course)

Stacked on #9 (t10-sirius) → … → #1. Merge those first.

What

  • src/providers/grades.ts:
    • fit.grades.courses({ semester? }): a per-course summary with pointsTotal, finalScore (mark A–F or points), assessment (whether the zápočet/credit is granted) and the raw overview items. Items carrying an upstream error are never used as the headline value.
    • fit.grades.course({ code, semester? }): the full breakdown as a depth-first tree (id/parentId/depth), with name, type, value, min/max/required and the teacher's note. Hidden items are excluded (showHidden=false, plus a client-side filter). Orphans go to the root, and cycles can't loop forever.
    • The semester defaults to the current one (resolved via semester-code). If that fails, it falls back to the server default.
    • The fit.grades namespace docs explain KOS semester codes (B251 = winter 2025/26) and value types.
  • The Grades API is undocumented. I took the endpoints from the public grades.fit.cvut.cz frontend bundle (MyClassificationService) and recorded them, with what still needs verifying, in docs/apis.md, which now also indexes the Usermap and Sirius sources. Only student endpoints under cvut:grades:user-read are used. Nothing teacher-only or course-restricted.
  • Course codes are validated (^[A-Za-z0-9]{2,5}-[A-Za-z0-9.]{1,15}$, uppercased and URL-encoded), as are semesters (^[AB]\d{3}$) and usernames. Parsing is lenient (values can be number, string, boolean or null), and real shape drift returns "unexpected response".
  • Search tweak: "zápočet" moved from the Sirius keywords to fit.grades.courses, because "do I have the zápočet" is a grades question.

Verified

  • bun run lint && bun run typecheck && bun run test: 152 tests, 9 of them for Grades. They cover the summary mapping and error items, the semester resolution URL, an explicit semester, fallback without one, invalid semesters, shape drift, tree order, hidden items, orphans, cycles, 404, course-code injection, and Czech and English search.
  • bun run dev + curl /api/functions lists 7 functions.
  • Not yet run against a real token. That waits for the browser login (T7); the open points are in docs/apis.md.

Astra review (gpt-6-astra)

"No concrete, unintended defects found." It confirmed that input validation prevents path injection, requests stay scoped to the connected user, and the safe HTTP error handling is reused. It noted that live-API compatibility is unverified, as documented.

🤖 Generated with Claude Code

## T11: Grades provider (`fit.grades.courses`, `fit.grades.course`) **Stacked on #9 (`t10-sirius`) → … → #1.** Merge those first. ### What - `src/providers/grades.ts`: - **`fit.grades.courses({ semester? })`:** a per-course summary with `pointsTotal`, `finalScore` (mark A–F or points), `assessment` (whether the zápočet/credit is granted) and the raw overview items. Items carrying an upstream `error` are never used as the headline value. - **`fit.grades.course({ code, semester? })`:** the full breakdown as a depth-first tree (`id`/`parentId`/`depth`), with name, type, value, min/max/required and the teacher's note. Hidden items are excluded (`showHidden=false`, plus a client-side filter). Orphans go to the root, and cycles can't loop forever. - The semester defaults to the current one (resolved via `semester-code`). If that fails, it falls back to the server default. - The `fit.grades` namespace docs explain KOS semester codes (B251 = winter 2025/26) and value types. - **The Grades API is undocumented.** I took the endpoints from the public `grades.fit.cvut.cz` frontend bundle (`MyClassificationService`) and recorded them, with what still needs verifying, in **`docs/apis.md`**, which now also indexes the Usermap and Sirius sources. Only student endpoints under `cvut:grades:user-read` are used. Nothing teacher-only or `course-restricted`. - Course codes are validated (`^[A-Za-z0-9]{2,5}-[A-Za-z0-9.]{1,15}$`, uppercased and URL-encoded), as are semesters (`^[AB]\d{3}$`) and usernames. Parsing is lenient (values can be number, string, boolean or null), and real shape drift returns "unexpected response". - Search tweak: "zápočet" moved from the Sirius keywords to `fit.grades.courses`, because "do I have the zápočet" is a grades question. ### Verified - `bun run lint && bun run typecheck && bun run test`: 152 tests, 9 of them for Grades. They cover the summary mapping and error items, the semester resolution URL, an explicit semester, fallback without one, invalid semesters, shape drift, tree order, hidden items, orphans, cycles, 404, course-code injection, and Czech and English search. - `bun run dev` + `curl /api/functions` lists 7 functions. - Not yet run against a real token. That waits for the browser login (T7); the open points are in `docs/apis.md`. ### Astra review (gpt-6-astra) "No concrete, unintended defects found." It confirmed that input validation prevents path injection, requests stay scoped to the connected user, and the safe HTTP error handling is reused. It noted that live-API compatibility is unverified, as documented. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- fixtures:record: reads the encrypted token from local D1, GET-only, never refreshes,
  scrubs PII into git-ignored test/fixtures/live/, prints shapes only
- Marast: FIT token goes in Api-Key (Bearer is 401); only_open is 401 → filter locally
- Usermap: fields may be null (preferredEmail) → nullish schemas
- Grades: precise error for missing OAuth app resource; docs on scopes vs. resources

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also fixes a production bug found while smoke-testing: createFnContext stored the global
fetch on the context object, so ctx.fetch(...) threw "Illegal invocation" in workerd. That
broke every registry call in the Worker (including all MCP tool calls); Node tests could
not see it. Regression test emulates workerd's this-check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ALLOWED_USERS enforced for MCP too (shared policy); excluded users' grants revoked on use
- telemetry: streaming 512-byte cap before parsing
- POST /oauth/register rate-limited per IP and instance-wide (Workers rate-limit bindings)
- FIT API bodies capped at 2 MB (streaming); Grades tree walk iterative and bounded
- readBounded moved to src/lib/bounded.ts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Logos: FIT service marks vendored in public/services (see README there); assistant marks
from @lobehub/icons (MIT), simple-icons (CC0) and devicon (MIT).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- service_settings table (migration 0002); MCP calls to a switched-off service fail with
  service_disabled, aggregates skip it with a warning, fit.status lists it
- setServiceEnabledFn: same-origin + session only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- fit.sirius.courseEvents: every parallel of a course (room, teachers, capacity/occupied),
  shared paging helper with fit.sirius.events; respects the Sirius MCP switch
- getCourseWeek server fn for the calendar overlay; upcoming deadlines (60 days) beside the week

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- layoutLanes/pragueMinutes in src/lib/calendar.ts (tested)
- shared EventDetails popover; agenda list stays on mobile
- agentSetupPrompt in src/mcp/setup.ts (tested)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Referrer-Policy: same-origin (no-referrer made browsers send Origin: null on our form POSTs,
  so every management form was rejected with 403); shared isSameOriginRequest accepts
  Origin: null only with Sec-Fetch-Site: same-origin
- No COOP on OAuth/consent/login routes (popup-based web MCP clients keep window.opener)
- fit.sirius.icalUrl is websiteOnly: excluded from the MCP registry (URL embeds a credential)
- docs: telemetry wording (operator-enabled, consent advice), CSRF/COOP/credential notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-on: #25
Reviewed-on: #24
Reviewed-on: #23
Reviewed-on: #22
Reviewed-on: #21
Reviewed-on: #20
Reviewed-on: #19
Reviewed-on: #18
Reviewed-on: #17
Reviewed-on: #16
Reviewed-on: #15
Reviewed-on: #14
Reviewed-on: #13
Reviewed-on: #12
Reviewed-on: #11
dzenda merged commit dde152de53 into t10-sirius 2026-11-14 17:50:24 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dzenda/os!10
No description provided.