plexams.go is a GraphQL/REST server for planning university exams
(Prüfungsplanung) at HM (Hochschule München, FK07). It imports exam and teacher
data from the ZPA system and student registration/conflict data from
Primuss, connects them, and helps schedule exams, assign rooms and plan
invigilations (Aufsichten).
It is the backend for plexams.gui and
is driven entirely through that GUI. Most domain terminology and log messages are
in German.
The former Cobra CLI has been removed: all functionality is now exposed as GraphQL queries/mutations/subscriptions or REST endpoints and driven from the GUI. It remains a single-user, local tool (no auth, localhost-only CORS).
Layers, each its own package, wired together in bootstrap/ + main.go:
bootstrap/— server entrypoint.main.gocallsbootstrap.Serve(), which parses the three flags, loads config and constructs the central*plexams.Plexamsinstance, then starts the server. (Replaced the former Cobracmd/package.)plexams/— business logic. Almost all domain operations are methods on*Plexams, grouped by concern across many files.db/— MongoDB persistence (mongo-driver). Each semester is its own MongoDB database (named e.g.2026-SS); rooms and NTAs live in a sharedplexamsdatabase.zpa/— HTTP client for the external ZPA REST API.graph/— GraphQL API generated by gqlgen.
See CLAUDE.md for more detail on the internals.
- Go (see
go.modfor the required version) - A running MongoDB instance
- Access to the ZPA and Primuss systems (credentials in the config)
go build -o plexams.go . # build the binary
go test ./... # run all tests
go vet ./...
golangci-lint-v2 run # the linter used in CI/pre-commit
go generate ./... # regenerate gqlgen code after editing *.graphqlsInstall the pre-commit hooks once (gofmt, go vet, golangci-lint-v2,
gitleaks):
pre-commit installConfig is loaded via viper from a single file
.plexams.yaml (in . or $HOME), which may optionally pin the semester
(e.g. 2026-SS). There is no per-semester <semester>.yaml anymore — the
per-semester config lives in the database and is edited through the GUI.
Key sections consumed by the code:
| Section | Purpose |
|---|---|
semester |
optional pin of the active semester, e.g. 2026-SS (else auto-selected from the DB) |
db.uri, db.database |
MongoDB connection |
zpa.* |
baseurl, username, password/token, fk07programs, oldprograms |
smtp.* |
mail server + testmail (dry-run recipient) |
jira.* |
on-prem Jira (jira.cc.hm.edu) integration: baseurl, token (PAT), project (default key FK07PP); url feeds the jiraURL email helper |
planer.* |
name/email of the planner (bootstrap/fallback; lives in DB) |
operator.* |
name/email of the local operator running this instance; stamped onto the mutation_log (audit "who did what"). Local per planner, never in DB |
server.* |
port, allowedorigins |
The per-semester planning config (from/until, slots, forbiddenDays,
emails, MUC.DAI slots) is not in the YAML — it lives in the DB
(semester_config_input) and is edited through the GUI.
The only command-line surface is three flags: -v/--verbose, --db-uri,
--semester. --semester optionally pins a workspace; without it the active
workspace is auto-selected from the DB (and can be switched at runtime from the
GUI via the setSemester mutation).
Secrets (ZPA token/password, SMTP password, Jira PAT) live in the config file, not in the DB. Keep the file out of version control.
plexams.go is a server only — it has no subcommands anymore. Running it
starts the GraphQL/REST server that plexams.gui talks to (GraphQL playground at
/, queries/mutations at POST /query, subscriptions over websocket, and REST
up/download routes on the same router; default CORS/origin allows
localhost:5173/8080/3000):
plexams.goEverything the old CLI did is now a GraphQL query/mutation/subscription or a REST endpoint, driven from the GUI.
A typical semester runs roughly in this order, all from the GUI:
- Create the semester (
createSemestermutation) and fill in the per-semester config. - Import from ZPA — the
importExamsFromZPA/importTeachersFromZPA/importInvigilatorRequirementsFromZPAsubscriptions. - Select exams to plan and add constraints.
- Import from Primuss (upload the Sammellisten ZIP to
/upload/primuss-zip) and connect the exams, then generate the assembled exams and student regs (generatePreparation/generateAssembledExams/generateStudentRegs). - Schedule — place exams (
setExamTime), check with thevalidateConflicts/validateConstraintssubscriptions. - Rooms — assign rooms (
assignRoomsForExamssubscription); request building-management rooms (see below). - Validate & upload — run the
validate*subscriptions, then upload withuploadExamsToZPA(…WithRooms/WithInvigilators); publish the plan and send the announcement emails. - Invigilations — collect requirements, generate the invigilation plan
(
assignInvigilationssubscription), validate, and send the published-invigilations emails.
Some rooms must be requested externally: T-building rooms via Anny, the
others via the Gebäudemanagement (building management). The management
requests are managed in the DB (collection room_requests, per semester):
- Dry run: the
roomRequestsPreviewquery shows which rooms would be requested for which exams — read-only, changes nothing. - Apply once (
applyRoomRequestsPreviewmutation): writes the requests; it refuses to overwrite existing ones unless forced, so an already-approved set is not lost. Add extras withaddRoomRequest, extend a request (e.g. for an NTA) withupdateRoomRequestTime. - Approve / deactivate individual requests as the management responds.
- Send the request email: the
sendEmailRoomRequestssubscription (withrun: falsefor a dry run tosmtp.testmail). RequiressemesterConfig.emails.roomManagement.
Stored time ranges include a 15-minute buffer before and after the exam for setup and teardown.
The full surface is the GraphQL schema in graph/*.graphqls (queries, mutations,
and LogLine-streaming subscriptions for imports, generation, validation, emails
and ZPA upload) plus the REST routes registered in
graph/server.go:
| Route | Purpose |
|---|---|
POST /upload/primuss-zip, /upload/email-attachment(s-zip) |
Primuss Sammellisten ZIP, email-attachment uploads |
POST /upload/jira-attachment |
attach an uploaded file (PDF/CSV) to a Jira issue (multipart: key, file) |
GET /download/planned-rooms.json |
planned rooms export (for external cover-page generation) |
GET /download/pdf/{kind} |
draft/plan PDFs (exams-to-plan, constraints, draft-fk08/fk10/exahm/muc.dai/fs/lba-rep, same-module-name; draft-si returns a ZIP) |
GET /download/csv/{kind} |
draft CSVs (draft?program=…, exahm, lba-repeater) |
GET /download/ics/{program} |
per-program exam calendar (ICS) |
| `GET/POST /download | upload/semester-dump.zip, /dataset, /dataset-csv, /my-inputs-csv.zip` |
Emails and ZPA upload run as subscriptions with a run/dryRun argument; with
run: false/dryRun: true they only mail the smtp.testmail recipient / do not
upload.
The invigilation planning (simulated annealing) and the SEB/EXaHM pre-planning (DSATUR + SA repair) are documented in docs/algorithmen.md (German).
The GraphQL layer is generated by gqlgen (gqlgen.yml). Edit the schema in
graph/*.graphqls, then run go generate ./.... Do not hand-edit the
generated files (graph/generated/generated.go, graph/model/models_gen.go,
the resolver signatures in graph/*.resolvers.go). Hand-written model types live
alongside the generated ones in graph/model/ and are autobound.
BSD 3-Clause License — see LICENSE.