Publish and add to Stemin
Stemin has no server for content. The browser of each learner reads your repository straight from its host, compiles it on the device, and keeps it there. So you publish a course by pushing it to a public repository.
The hosts
The app reads three kinds of host. It knows the kind of the well-known hosts from the host name in the address:
| Host name | Read as | File list from | Files from |
|---|---|---|---|
github.com | GitHub | api.github.com | raw.githubusercontent.com |
gitlab.com | GitLab | the GitLab API, v4 | the GitLab API, v4 |
codeberg.org, gitea.com | Forgejo or Gitea | the Forgejo API, v1 | the Forgejo API, v1 |
| any other host | found by a probe | the API it finds | the API it finds |
A server of your own works if it runs Forgejo, Gitea or GitLab. The first time a device sees such a server, the app asks it two questions, at most:
GET /api/v1/version. A Forgejo or Gitea server answers with its version.GET /api/v4/projects/<owner>%2F<name>. A GitLab server answers, also for a project that it does not show.
The device keeps the answer for that server, and the account keeps it for the repository. So a probe that finds the kind costs one or two requests, one time. A probe that finds nothing runs again the next time. A GitLab server sends CORS headers on /api/v4 by default.
The app always uses HTTPS.
The repository must be public
The app reads the repository with no account. GitHub gives the same answer for a private repository and for an address that does not exist, so the learner sees:
GitHub shows no repository at `github.com/alice/algebra`. Check the address. A private repository gets the same answer, and Stemin reads only public repositories.
A host that refuses a visitor who is not signed in gives:
GitHub refuses to show `github.com/alice/algebra` to a signed-out browser. Stemin reads only public repositories.
The host must also let a browser read its replies: it must send CORS headers. GitHub, GitLab and Codeberg do. A Forgejo or Gitea server of your own needs this in its app.ini:
[cors]
ENABLED = true
Without it, the learner sees:
Forgejo sends no `Access-Control-Allow-Origin`, so a browser may not read it. A self-hosted Forgejo or Gitea needs `[cors] ENABLED = true` in its `app.ini`.
The app shows the same message when the network fails.
The app downloads every .md file and every picture of the repository. It downloads no other file, so a licence or a CI workflow costs the learner nothing. A picture outside an assets/ directory is a fault, as in stemin check. A .md file outside the tree, for example a README.md, costs one download, and the compile ignores it.
The address
The learner writes the address of the repository. These forms all name one repository:
github.com/alice/algebra
https://github.com/alice/algebra
https://GitHub.com/Alice/Algebra.git
git@github.com:alice/algebra.git
https://github.com/alice/algebra/tree/main
https://github.com/alice/algebra?tab=readme-ov-file
The app removes the scheme, a user name, a query, a trailing / and a trailing .git. It removes a browse path too, such as /tree/main (GitHub), /-/tree/main (GitLab) or /src/branch/main (Forgejo). Then it makes the address lowercase. So a learner can paste the address from the address bar of the browser.
- A GitLab subgroup stays:
gitlab.com/group/subgroup/courseworks. - A port stays:
git.example.com:3000/alice/algebraworks. - An address without an owner and a name is refused:
`github.com/alice` does not name a repository: expected something like `github.com/owner/name`.
The ids of your domains come from this address (The tree). If you move the repository to another host, owner or name, its domains take new ids. The learners must add it again, and their progress does not follow.
Add it in the app
Your first repository gives the steps. In short:
- The learner presses Add on the welcome page, or the
+in the header. - The learner writes the address in Repository address.
- The app reads the repository. It lists the branches and the tags in Branch or tag, and it shows the
description:of your rootindex.md. - The learner keeps Install on this device on, and presses Add.
When the app reads the address, it reads your root index.md, before it downloads anything else. For a root with no format:, or with a format it cannot read, it shows the fault under the form, and Add stays off until the address changes.
The list of repositories belongs to the account of the learner, on every device. A learner with no account has the list on one device only. The install belongs to one device. Another device shows the same list, and installs a repository when the learner asks.
Branch or tag
Branch or tag lists the branches first, then the tags, up to 100 of each. A tag shows a ⌾ after its name. The app selects main, else master, else the first ref in the list.
| The learner picks | The learner gets |
|---|---|
| a branch | your latest push to it, at each refresh |
| a tag | the content at that tag, until you move the tag |
Keep your live course on main. To publish a stable version, make a tag, for example v1.0. A learner can change the ref later: in Repositories, they press the ref beside the name of the repository and pick another one. The ref belongs to the account, so every device of the learner follows it at its next refresh.
The ref is not part of the id of a domain. A branch and a tag of one repository give the same domains, and the progress follows the rules of a refresh, below.
The import
The import reads, downloads, compiles and saves. The button of the repository, on the welcome page or in Repositories, shows the stage: Reading, Downloading, Compiling, Saving. The learner can study while it runs.
- Reading. The app asks the host for the commit of the ref. Then it asks for the list of files at that commit, with the git hash of each file.
- Downloading. The app downloads the files it does not hold yet, 16 at a time.
- Compiling. The app runs the checker and the compiler on the device (Check your repository).
- Saving. The app saves the result on the device.
If any step fails, the import stops and saves nothing.
A refresh
A refresh imports the repository again at its ref. The learner presses Refresh in Repositories. A dot after the name of a repository says that the host holds a newer commit. The app asks each host for this once every six hours at most.
A refresh does only the work that your push needs:
- If the ref points to the commit the device holds, the refresh stops.
- Otherwise the app compares the git hash of each file with the hash it holds. It downloads only the files with content that it does not hold. A typo fix costs the learner one file. A renamed or copied file costs nothing: the device holds its content already.
- The app then compiles the whole repository again, from the files it holds and the files it downloaded.
If the compile fails, the learner keeps the old content. The row shows The import failed., and the content from the last good import stays on the device. A broken push does not break a course for a learner who already installed it. A learner who installs for the first time gets nothing until you fix it.
What a refresh does to progress
The app compares the content the learner holds with the new content, unit by unit:
| Change | Progress |
|---|---|
| a deck with the same id | kept, also when its cards changed |
| a deck with a new id and exactly the same cards, in the same order | moved to the new id |
| a deck that is gone | cleared |
| a flashcard with the same id and the same content | kept |
| a flashcard with the same id and new content | its schedule is cleared |
| a flashcard with a new id and the same content | moved to the new id |
| a flashcard that is gone | its schedule is cleared |
| a domain directory with a new name | moved, if more than half of its decks keep their cards; the exam progress of the domain is cleared |
A deck with no cards is known by its flashcards instead. The content of a flashcard is its question, its answer and its solution together.
When a refresh removes a deck, renames a deck or clears a review card, the app asks first. It counts all the content, not only the content the learner worked on. It shows What this refresh changes, with a count for each change above zero: Decks removed, Decks renamed, keeping their progress, Decks changed, and Review cards cleared. The learner presses Refresh or Cancel. Any other refresh applies at once, also one that only changes the cards of a deck.
So:
- Give each flashcard an id, and keep it (Flashcards and solutions).
- To rename a deck, change its file name and nothing else in that commit.
- Edit a flashcard only when the edit matters to the learner.
Rate limits
GitHub allows 60 API requests an hour from one network address to a visitor who is not signed in. All the learners behind one address, for example in a school, share these 60. On GitHub, the files come from raw.githubusercontent.com, which does not count. So the size of your repository does not change the cost.
| Action | API requests on GitHub |
|---|---|
| the add modal reads an address | 2: the branches and the tags, which also name the commit of each ref |
| the first install | 2: the commit, and the list of files |
| a refresh with no new commit | 1 |
| a refresh with a new commit | 2 |
| the check for a newer commit | 1 per installed repository, once every six hours at most |
| the welcome page, which shows when this device holds no domain | 1 per repository in the list, each time the app loads |
| the list of refs in Repositories | 2 |
Before the app reads the list of files, it reads the budget that the host reports. If nothing is left, it stops with:
this host allows 60 requests an hour from one address, and they are spent. Try again in twenty minutes.
GitLab and Forgejo set limits of their own, and the app reads their budget the same way. On these hosts, each file comes through the API too, so each file is one request. They also send the list of files in pages of 100 files at most, one request per page.
Size limits
| Limit | Message |
|---|---|
| GitHub gives up on a very large tree, at about 100,000 entries | this repository is too large for its host's tree API to describe in full |
| GitLab and Forgejo: more than 200 pages of files | this repository has more than 200 pages of files, which is more than an import reads |
| a picture over 256 KB | the checker fault (Math, figures and assets) |
What a learner sees on an error
In the add modal, the app shows the message under the form. On the welcome page, a failed install shows The import failed. and every fault, in full. In Repositories, the row shows The import failed., and the faults show when the learner points at it.
| When | Message |
|---|---|
| the address names no repository | `github.com/alice` does not name a repository: expected something like `github.com/owner/name` |
| the host sends no CORS headers, or the network fails | Forgejo sends no `Access-Control-Allow-Origin`, so a browser may not read it. A self-hosted Forgejo or Gitea needs `[cors] ENABLED = true` in its `app.ini`. |
| the repository is private, or the address is wrong | GitHub shows no repository at `github.com/alice/algebra`. Check the address. A private repository gets the same answer, and Stemin reads only public repositories. |
| the ref does not exist | `github.com/alice/algebra` has no branch or tag `v2`. Check the ref. |
| the host cannot tell the repository from the ref | GitLab shows no repository at `gitlab.com/alice/algebra` with the branch or tag `v2`. Check the address and the ref. A private repository gets the same answer, and Stemin reads only public repositories. |
| the host refuses a signed-out visitor | GitHub refuses to show `github.com/alice/algebra` to a signed-out browser. Stemin reads only public repositories. |
| the host gives another error | GitHub answered 500 for `github.com/alice/algebra`: check the address and the ref |
a file does not download, for example a missing root index.md | GitHub answered 404 for a file |
the root has no format: | this repository declares no `format:`, so it is not a content repository |
| the format is not one this app reads | this repository targets format 1.0.0, and this app reads 2.x |
| the rate limit is spent | this host allows 60 requests an hour from one address, and they are spent. Try again in twenty minutes. |
| the repository is in the list already | This repository is already in your list. |
| the server refuses the address | No adapter reads that host yet. GitHub, GitLab, Forgejo and Gitea work. |
| the content has faults | every fault, with its file and its line, as stemin check gives them (Check your repository) |
A learner cannot fix your content. So run stemin check before each push, and keep the CI green.