The web console¶
pupitre-web (module pupitre.webd, unit pupitre-web.service) is the supervision page of a
Pupitre server. Open http://127.0.0.1:8080 on the server, or from your own machine through
ssh -L 8080:127.0.0.1:8080 server. It is a single page, refreshed every 3 s.
Security, in plain words¶
The console shows the live screen of every seat, whatever a pupil has on screen, passwords included, and it controls every client: it assigns seats, stops sessions, renames devices and restarts seats. Whoever reaches it is an administrator.
Two supported ways to run it, and nothing in between.
On loopback, without a password. The default, and the one the ssh -L tunnel above uses.
Whoever reaches 127.0.0.1 already has an account on the server, so a password would add a step
without adding a guarantee.
On the network, with a password. What a school needs, because a technician has no ssh tunnel. Set one:
pupitre-web --mot-de-passe /etc/pupitre/console-password
It asks twice, writes the hash in mode 600, and prints nothing else. Then point [web]
password_file at that file and set [web] listen. There is deliberately no way to pass the
password on the command line: it would be visible to everyone in ps and kept in the shell
history.
The console REFUSES to start on a non loopback address without a password, and exits 2
saying so: a mere warning is not read at the moment it matters, and an unauthenticated
console could be exposed by inattention. It also refuses a password
file readable beyond its owner, because a hash is still attackable offline and a 0644 in /etc
is a common enough mistake to deserve a refusal rather than a warning.
How it behaves once a password is set. /login serves a form; a page asked without a session is
redirected there, while an API call gets 401, so a technician never sees raw JSON and the
page can reopen a session without replacing its data with HTML. The session cookie is HttpOnly,
SameSite=Strict, and lives four hours. Five failed attempts per source per five minutes, after
which even the right password is refused for a while. Sessions live in memory only: restarting
the console logs everyone out. Changing the password invalidates every session, and that is the
only revocation there is.
The Host, Origin and Content-Type checks still apply to every action, session or not: they
are what stops a page in your own browser from driving the console across origins, and they apply
to the login itself.
What this does NOT give you¶
- No encryption. The password and the session token travel in clear HTTP. On an admin VLAN that is a defensible choice; on the pupils' network it is not. A TLS reverse proxy is the answer, and pupitre does not provide one.
- No accounts. One shared password, so no trace of WHO acted. A single classroom has one technician; an authority with several will want accounts.
- No audit log. Actions are logged, the person behind them is not.
Configuration¶
/etc/pupitre/pupitre.conf, section [web]:
| Key | Default | Meaning |
|---|---|---|
listen |
127.0.0.1 |
address the console binds |
port |
8080 |
TCP port |
password_file |
empty | file holding the password hash; required for a non loopback listen |
The rest comes from the same file as the manager: [server] interface, ip, seats (15 by default, 0 means
no seat limit), enroll, and the paths of the seats table and the run directory.
The page¶
Header. 13 seats, 11 assigned, 9 in session, 2 stations without a seat, the enrolment
switch as configured, and a dot: green while the console answers, red with Console
unreachable after 10 s without an answer. A red manager not running badge appears within
about 10 s of the manager's heartbeat stopping (/run/pupitre/manager.alive).
Seat cards. One card per seat number in the table: the seat, its label (click to edit, Enter saves, Escape cancels), a badge, the live thumbnail (refreshed every 10 s, click for the 640 px view refreshed every 3 s, Escape or click closes it), the device name, serial, MAC, IP, when it was last seen, the feedback strip and the buttons.
| Badge | Meaning |
|---|---|
| In session | the seat unit runs and the launcher reported ready |
| Starting 2/5 | the unit runs, the launcher is on attempt 2 of 5; plain Starting while systemd waits to restart the unit |
| Stopped | the unit is inactive |
| Held (renaming) | the console is talking to the device, the manager leaves it alone |
| Failed | systemd refuses to start the unit again: more than 5 starts within 300 s. Only a seat that fails within seconds gets there (a missing seat env file, a display that will not open). A seat whose client never reaches the live stream spends at least 375 s of launcher cooldowns in each start, so it cycles through Starting and never shows Failed. The manager resets a failed unit and starts it again itself, waiting twice as long each time up to 900 s; Restart resets it at once |
| Offline | no broadcast from the client for 30 s (card dimmed) |
| Never seen | the MAC is in the table but the client never announced itself (dimmed) |
| Button | What it does |
|---|---|
| Identify | in session: a big Seat N on the client's screen for 5 s; stopped or failed: the device is temporarily renamed SEAT-N for 20 s, then its previous name is restored (refused on a device still carrying its factory name, rename it first). The device's waiting screen does not show its name, so this rename has no visible effect on the client |
| Restart | reset-failed then restart of the seat unit; the client goes dark for 30 to 90 s |
| Relink | rebuilds the device link without touching the desktop (SIGUSR1 to the seat's launcher); the pupil keeps their session; 15 to 60 s |
| Logs | the tail of one of the seat's logs (session, encoder, desktop, Xvfb, input, cursor, key), at most 500 lines; the client's log, which carries input reports, is never served |
| Rename device | the name the device announces in its broadcasts (its waiting screen does not show it), 1 to 16 printable ASCII characters; only on a stopped or failed seat, unassign first otherwise; the client briefly leaves its waiting screen during the dialogue |
| Label | free text kept on the server, never sent to the device |
| Unassign | stops the seat and parks the station: it keeps its label, is listed below, and is not auto enrolled again |
Stations without a seat. New and parked stations, with a seat picker (the lowest free
number is preselected, other… takes any number), Assign, Identify, Rename device, Label, and
Forget for a parked station (with enrolment on, a forgotten station gets a seat at its next
announce).
Feedback. While an action runs its card is locked and shows the progress message; a green strip stays 8 s on success, a red one until dismissed on failure. A rename, or an identify on a seat that is not in session, is confirmed by the device itself: the name it announces in its next broadcast is the proof.
Two browser tabs, or two operators, cannot double an action: one action per client at a time,
the second one is answered busy.
Dev mode¶
pupitre-web --state-dir DIR --no-systemd --port 0 runs the console unprivileged against a
directory (DIR/seats.json, DIR/run/, DIR/units.json for the unit states) and prints
listening on 127.0.0.1:PORT. systemctl calls, the DCP dialogue and the overlay are logged
instead of run, and the identify hold lasts 2 s instead of 20 s.
The Playwright scenarios of tests/e2e/ run against it. Playwright is not a Debian dependency
and numpy comes from python3-numpy, so use a venv that sees the system packages:
python3 -m venv --system-site-packages .venv
.venv/bin/pip install playwright
.venv/bin/playwright install chromium
PUPITRE_PLAYWRIGHT=1 .venv/bin/python -m pytest tests/e2e -m e2e
Without PUPITRE_PLAYWRIGHT=1 the scenarios are skipped, and the Debian build runs
pytest -m "not e2e".