A per-user jz_bm_material_pins table and PUT/DELETE /materials/{id}/pin
on the student API and its token twins: 204 for a material the caller
may see, idempotent, 404 otherwise. List rows, course rows and the
detail carry pinned. The plain list lists the caller's pins first
(newest pin first); with q the relevance order stays. A pin writes only
the pins table and never changes the material's revision.
- jz.bm.realtime-access admits holders of jz.bm.access_materials
(Assumption A5), so every editor can follow materials:{id}
- README documents the materials channel, event and authorizer
- every revision bump (API writes and /bmadmin saves) enqueues
updated.jz.bm.material with {material_id, revision} in a savepoint
- the materials namespace authorizer allows a subscribe only to a
visible material the user may manage
- api.ManagesMaterials and api.CanWatchMaterial
- GET /_bm/api/v1/tags: the jz_bm_tags dictionary, to material managers
- POST /materials/{id}/unpublish: back to drafts, no revision bump, the
search document resynced
- POST /materials/{id}/images: one JPEG/PNG/GIF/WebP per request through
storeMaterialUpload, refusals with reason type, size or count
- storeMaterialUpload/fileRefusalFor: attach.ErrNotImage is a type
refusal, not a 500; storeMaterialFile keeps its texts
- the author's routes get their own per-user buckets (the inline
throttle form shares one counter per user)
- README: the author routes and revision on the student detail
- PUT /_bm/api/v1/materials/{id} (jwt.auth only, no token twin): every
key of the edit body required, revision checked on the row locked FOR
UPDATE, 409 with the student detail of the server version
- editableMaterialForCaller: 404 for what the caller cannot see, 403 for
what they see but may not manage
- tags from the dictionary only (at most 4), http(s) urls, the body
normalised as bm-notatka push does; course, author and publication kept
- loadMaterialDetail shared by GET /materials/{id} and the edit answers
- POST /materials locks the matched row and answers 409 to a stale
revision when it updates; a create ignores revision
- bm_manage_save_material and bm_manage_update_material take revision
- /bmadmin saves bump the revision on content changes (not checked)
- document revisions and the 409 in the README
- add jz_bm_materials.revision (default 1), read-only for GORM
- bump it only on content changes (models.ReviseMaterial)
- PUT /materials/{id} locks the row and answers 409 to a stale revision
- management answers carry revision and updated_at, the student detail
carries revision
The backend materials list shows the zjazd (meeting, by number). The
MCP save and update material tools, the README material example and the
management token test stop modelling a zjazd as a tag, and the README
describes the one-way zjazd tag migration.
The one-way migration 20261108140000_move_zjazd_tags_to_meetings gives
every material tagged zjazd-N without a meeting meeting N (the lowest
when it has several), never overwrites a meeting_id, logs tags naming a
missing meeting or disagreeing with meeting_id at Warn, and deletes the
exact zjazd-N tags; their links go through ON DELETE CASCADE. Rollback
is a no-op: the backup is the pg_dump taken before migrate.
- inline and signed uploads queue the text job in their transaction
- the admin form queues it in the save transaction
- detach resyncs the material (its text row goes by cascade)
- bm:materials-reindex backfills missing PDF texts before each sync
- jz_bm_material_file_texts caches the pdftotext output per file
- the River job jz.bm.extract_material_text extracts and resyncs
- the management multipart attach queues it inside its transaction
- file_texts in the document and a file snippet with its name
- GET /api/v1/bm/materials takes q: candidates over every material, in
rank order, with the snippet of the student list; 503 when the engine
cannot answer
- bm_materials and bm_manage_list_materials take q; under MCP the snippet
is plain text with ellipses where text was cut
- MCPMaterialPrivacy no longer claims no file text is ever returned
- README documents q, the snippet, the 503, the Search section and
bm:materials-reindex
- the materials admin syncs after create and update and removes after
delete, once the save transaction commits (UseMaterialIndex, set at Boot)
- POST and PUT /api/v1/bm/materials sync after the transaction, DELETE
removes the document (its statement has no primary key, so no callback)
- bm:materials-reindex [--fresh] [--if-configured] ensures the index and
syncs every material; it refuses without an engine and runs nightly
at 04:15 with --if-configured
- TestMaterialSearchAgainstDatabase (Postgres) is written for the final
verification phase
- Material is a beachcomber Searchable (index jz_bm_materials) in the
hand-written models/material_search.go; jz.bm requires summercms v0.6.4
- classes.MaterialIndexer asks Typesense for up to 250 candidates with
body highlights; the app-wide Gate is on only for jz.bm-marked contexts
- GET /_bm/api/v1/materials takes q: candidates are re-gated, counted and
paged in SQL in rank order, each row with at most one plain snippet
- search failure answers 503 Search is unavailable.; without q the list
is unchanged
The management save/update body takes is_private, author_user_id and
share_user_ids (absent keeps each; a new material gets the caller as
author), and the managed material shows them. New routes PATCH
/materials/{id}/visibility, PUT /materials/{id}/shares and GET /users,
with the MCP tools bm_manage_set_material_visibility,
bm_manage_set_material_shares and bm_manage_list_users. The /bmadmin
form gets the private switch, the author picker and shared_with.
A material may be private: only its author and the users it is shared
with see it. Every student read (list, detail, course detail and
counters, the checklist linked material, the token twins) goes through
one visibility fragment. Existing materials stay public with no author.
GET/PUT /_bm/api/v1/materials/{id}/access let the author, a material
manager, toggle the flag and pick the users; the first change of an
author-less material makes the caller its author.
Per-user checklists with items that may link a course, reading, deadline,
material or own presentation deck. An item with a reading shares the
reading's read mark. Student API routes with token twins, MCP tools,
migrations, tests and README.
- classes/uploads.go: tickets signed with HMAC-SHA256 under the
jz.bm.upload.v1: label (app.key), 10-minute expiry, the nonce pulled
from the cache once (duplicated from golem15.presentations on purpose)
- POST /api/v1/bm/materials/{id}/files/upload-url issues the URL with a
curl example; POST /_bm/api/v1/uploads/{ticket} takes one file part,
no bearer, throttled per IP and per ticket, user and permission
re-checked, stored through storeMaterialFile
- bm_manage_create_material_upload; README documents the file routes
- TestUploadTicket: HMAC under the jz.bm.upload.v1: label, expiry,
malformed and foreign tickets, single use through the cache
- TestSignedMaterialUploadRefusesBadTickets, TestCreateMaterialUploadNeedsCaller
- TestRoutes, TestBMMCP and TestManageBucketKey expect the upload-url
route, the uploads route with its two buckets and the upload tool
- compile-only stubs
- storeMaterialFile: the one attach.Store call of the management API,
shared by the multipart route and the new inline route
- POST /api/v1/bm/materials/{id}/files/inline: JSON {name, mime?,
content_base64}, at most 3000000 bytes decoded, body.limit 4300000
- bm_manage_attach_material_file and bm_manage_detach_material_file,
both warning that material files are public to anyone with the link
- 28 typed tools over /api/v1/bm: list, show, create and update for
courses and meetings, plus delete for class sessions, readings,
deadlines and materials (save upserts on title and course)
- inputs mirror Fillable and Rules; enums, lengths and date formats in
the field descriptions; no seed, me or dry_run tool
- materials accept meeting_id (absent keeps, null clears, must exist)
and the managed material JSON shows it
- README: MCP tools section
- 28 typed management tools pinned in TestBMMCP
- body keys are Fillable columns, create inputs require the required
columns, material save/update require body, source and url
- no seed or me tool, deletes only where the route deletes
- every description names its scope and permission
- jz.bm.mcp-links on the token twins and the management API: under MCP,
url and *_url strings starting with a single / get the app base
- material answers under MCP leave the body out unless include_body,
marking it with body_omitted; write answers never echo it
- jz.bm.manage keys a dispatched call by user
- bm_materials and bm_material student tools
Plain HTTP answers are unchanged.
- viaMCP, includeBody and the material views (no body unless asked)
- jz.bm.mcp-links: absolute url / *_url under MCP only
- jz.bm.manage keyed per user for dispatched calls
- bm_material and bm_materials in the pinned tool list
Compile-only stubs; the tests fail on their assertions.
- token twins of the student API under /_bm/api/v1/token (user.api_token,
per-user jz.bm.token bucket, a scope on each), from one mountStudent
- pact.HasMCP with the eleven student tools over the twins
- TestBMMCP and TestTokenMirrorRoutes
/mcp (the MCP endpoint) and /.well-known (the OAuth metadata documents)
join reservedPrefixes, so they answer a plain 404 here instead of the
student app; /connect stays a client route.