Some Wonder Ink lesson links are a download rather than a page: the server sends the PDF as an attachment, so there is nothing for a tab to show and the file lands on disk. In this room that is the same as it not arriving. There is no file manager, no application launcher and no downloads bar, on purpose, and a teacher is not going to open a maintenance terminal to print a Christmas tree. 0.2.1 downloaded the worksheet, said so in a notification, and left it somewhere unreachable. Found on the canary machine with a real lesson. So a download that completes, and really is a PDF, opens as another tab. Rendering is the engine's own viewer — the same PDFium Chrome uses — so there is no second rendering library and no separate application. That was verified against a completed local file rather than assumed from the remote case: PdfViewerEnabled was already on, and a local file: URL is a different question. WHAT IS REFUSED. A completed download is not a usable worksheet. HTML that a server called .pdf, an empty file, an unreadable one: all refused on the first five bytes rather than on the name, because the alternative is a blank tab that says nothing. An interrupted download says it did not finish and to try again — and that needed stateChanged rather than isFinishedChanged, which is not emitted for an interrupted download at all. Connected to the obvious signal, this said nothing whatsoever about a worksheet that failed to arrive, which is the one case a teacher most needs told about. Measured. WHICH LOCAL FILES. Exactly the ones the window opened itself. Opening a worksheet adds one resolved path to a list, and the page's acceptNavigationRequest refuses every file: URL that is not on it. Chromium already refuses to let a remote page reach file: — verified with a link, a location.href, and a link to a worksheet that had been downloaded but not opened by the shell, all refused — so this is the second lock rather than the only one. It is here because the first one is somebody else's. Two Qt defaults are turned off beside it: LocalContentCanAccessFileUrls, which is on by default and would let a local document read other local documents, and its remote counterpart. Print to File is removed from the print dialog, because choosing it enables a path with a "..." button, which is a file chooser, in a room whose whole design is that there is no file chooser. The PDF viewer's own menus were audited: two-page view, annotations, present, properties, and nothing that opens or saves anywhere a teacher chooses. AND THE BROWSER IS NOT INVOLVED. The handler is connected to Wonder Ink's own profile, so it only ever sees downloads that started in a Wonder Ink tab. Nothing inspects a filename, nothing watches a directory, and Chrome — a separate application with a separate profile — is untouched: classroom-browser and the Chrome policies are byte-identical to stable 0.2.0. Worksheets go to ~/Downloads/Wonder Ink, which is tidiness and mostly the security argument: it is the only place one can come from. Only PDFs. Anything else downloads, says so, and is still unreachable; that limitation and two small follow-ups are written down rather than built, and nothing downloaded is ever executed. Measured from the package on a rebooted machine, and again on one upgraded from the published 0.2.1: the click-to-tab workflow end to end, a name with spaces and brackets, multi-page navigation, printing to Elmer, closing the worksheet without closing the lesson, and the four refusals. 120 convergence checks and 24 worksheet checks pass on both. Cookies are still session-only, the Present-view autoplay fix still works, fullscreen still comes back maximised, and there is still no address bar and no new-tab button. Co-Authored-By: Claude Opus 5 <[email protected]>
365 lines
15 KiB
Markdown
365 lines
15 KiB
Markdown
# Wonder Ink
|
||
|
||
Wonder Ink is a lesson application, not a browser: its own window, its own
|
||
profile, and no way from it into the rest of the web.
|
||
|
||
Since 0.2.1 it has tabs.
|
||
|
||
## Why it stopped being Chrome
|
||
|
||
It used to be `google-chrome --app=https://app.wonderink.org/`. App mode
|
||
gives exactly the window a classroom wants — no tab strip, no address bar,
|
||
reads as a program — and teacher feedback then found the thing app mode
|
||
takes away is the thing they need. A lesson is prepared across several
|
||
tabs: the lesson, its video, the activity.
|
||
|
||
Chrome cannot be asked for "tabs but no omnibox". App mode has no tabs. A
|
||
normal window has tabs and an address bar, and no policy, flag or
|
||
extension removes the address bar from it. Tabbed web apps come closest,
|
||
need a `display_override` in a manifest on a site that is not ours, and
|
||
push off-origin links out into a normal browser window — which is the
|
||
whole of what this design exists to prevent.
|
||
|
||
So the tab strip is the classroom's own, in `classroom-wonderink`, and the
|
||
engine is Chromium either way.
|
||
|
||
## Why QtWebEngine
|
||
|
||
`python3-pyqt6.qtwebengine`, from Debian, updated with the rest of the
|
||
system.
|
||
|
||
The usual and correct objection is codecs, so it was measured before it
|
||
was chosen. On Debian 13, QtWebEngine 6.8.2 (Chromium 122):
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| `canPlayType` H.264, AAC, MP3, VP8, VP9 | all "probably" |
|
||
| `MediaSource.isTypeSupported` avc1, mp4a, vp9 | all true |
|
||
| a 720p H.264/AAC file | loaded, played to the end, 1280×720 |
|
||
| `app.wonderink.org` | loads; the sign-in is an ordinary email and password form, not a Google flow that would mind an embedded browser |
|
||
|
||
Two further things the shell does for the same reason:
|
||
|
||
* **The user agent** has QtWebEngine's own token stripped, leaving a plain
|
||
Chrome string. The engine really is that Chromium, and a site gating on
|
||
the UA should not see something it has never heard of.
|
||
* **Widevine** is handed to it from Chrome's own bundle when Chrome is
|
||
installed, which on a classroom machine it always is. A lesson whose
|
||
video service wants DRM is not stopped by the engine change. Absent, the
|
||
flag is not passed and nothing else is affected.
|
||
|
||
## What a teacher gets
|
||
|
||
[ Lesson ] [ Video ] [ Activity ]
|
||
|
||
The strip appears only when there is more than one tab, so a room with one
|
||
lesson open looks exactly like the application this replaced. Each tab has
|
||
a close button. Closing the last one closes Wonder Ink; closing any other
|
||
leaves the rest alone.
|
||
|
||
A tab can only come from the lesson: a link the site opens in a new
|
||
window, a middle click, a Ctrl+click, "open in a new tab" in the menu.
|
||
There is no address bar, no search, and no button that makes a blank tab.
|
||
That is the seam the whole design turns on — Wonder Ink can take a teacher
|
||
anywhere it needs to, including a video CDN nobody told us about, and a
|
||
teacher cannot take themselves anywhere.
|
||
|
||
**There is deliberately no allowlist of domains.** A list would have to be
|
||
right about every service a lesson might ever embed, and the day it is
|
||
wrong a teacher is standing in front of a class.
|
||
|
||
The right-click menu is built rather than filtered, so a future Qt cannot
|
||
grow view-source or save-page back into it without somebody noticing in
|
||
the code: back, forward, reload, open in a new tab when there is a link,
|
||
cut/copy/paste in a text field, print.
|
||
|
||
Keys: Ctrl+W closes a tab, Alt+Left and Alt+Right navigate, Ctrl+P prints,
|
||
Ctrl+Tab switches. Ctrl+T, Ctrl+N and Ctrl+L are deliberately unbound.
|
||
|
||
## Worksheets
|
||
|
||
Some of Wonder Ink's lesson links are a download rather than a page: the
|
||
server sends the PDF as an attachment, so there is nothing for a tab to
|
||
show and the file simply lands on disk.
|
||
|
||
In this room that was the same as it not arriving at all. There is no file
|
||
manager, no application launcher and no downloads bar, on purpose, and a
|
||
teacher is not going to open a maintenance terminal to print a Christmas
|
||
tree. 0.2.1 downloaded the worksheet, said so in a notification, and left
|
||
it somewhere unreachable.
|
||
|
||
So a worksheet that finishes downloading is opened in a tab:
|
||
|
||
1. a teacher clicks a worksheet link;
|
||
2. Wonder Ink downloads it into `~/Downloads/Wonder Ink`;
|
||
3. when the download **completes**, and the file really is a PDF, it
|
||
opens as another tab, named after the file;
|
||
4. the teacher reads it, scrolls it, prints it to the room's printer;
|
||
5. closing that tab leaves the lesson exactly where it was.
|
||
|
||
Rendering is the engine's own PDF viewer — the same PDFium that Chrome
|
||
uses — so there is no second rendering library in the room and no
|
||
separate application. `QtWebEngineSettings.PdfViewerEnabled` was already
|
||
on for remote PDFs; what 0.2.2 adds is opening a *local* one, which was
|
||
verified against a real completed file rather than assumed from the
|
||
remote case.
|
||
|
||
### What is refused, and why
|
||
|
||
A completed download is not the same as a usable worksheet, and each of
|
||
these was a real case in the lab:
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| an interrupted download | the notification says it did not finish and to try again. Nothing opens |
|
||
| HTML that the server called `.pdf` | refused on its first five bytes, not on its name. Without this the teacher gets a blank tab and no reason |
|
||
| an empty or unreadable file | the same |
|
||
| anything that is not a PDF | saved, and said so, exactly as before. Nothing is opened and nothing is executed |
|
||
|
||
The terminal state is read from `stateChanged`, not `isFinishedChanged`.
|
||
Measured: an interrupted download never emits `isFinishedChanged` at all,
|
||
so the first version of this said nothing whatsoever about a worksheet
|
||
that failed to arrive — which is the one case a teacher most needs told
|
||
about.
|
||
|
||
### Which local files Wonder Ink will open
|
||
|
||
Exactly the ones it opened itself, and nothing else.
|
||
|
||
Opening a worksheet adds that **one resolved path** to a list. The page's
|
||
`acceptNavigationRequest` refuses every `file:` URL that is not on that
|
||
list, so a lesson cannot navigate to one, a link cannot point at one and a
|
||
redirect cannot reach one. Chromium already refuses to let a remote page
|
||
reach `file:` at all — verified in the lab, where a link, a
|
||
`location.href` and a link to a worksheet that *had* been downloaded but
|
||
not opened by the shell were all refused — so this is the second lock
|
||
rather than the only one. It is there because the first one is somebody
|
||
else's.
|
||
|
||
Alongside it, two Qt defaults are turned off rather than left implicit:
|
||
`LocalContentCanAccessFileUrls` (on by default, and would let a local
|
||
document read other local documents) and `LocalContentCanAccessRemoteUrls`.
|
||
|
||
The print dialog's **Print to File** option is also removed. Choosing it
|
||
enables an output path with a "..." button, which is a file chooser, in a
|
||
room whose whole design is that there is no file chooser.
|
||
|
||
### What a teacher still cannot reach
|
||
|
||
**Anything that is not a PDF.** A lesson handing out a Word document, a
|
||
spreadsheet, a zip of images or a PowerPoint still downloads, still says
|
||
so, and still lands somewhere a teacher has no way to open. This release
|
||
deliberately fixes only the worksheet case, because that is the one the
|
||
room actually hit and because "open whatever arrives" is how an appliance
|
||
stops being one.
|
||
|
||
The obvious small follow-ups, in the order they are worth doing:
|
||
|
||
1. **Presentations.** Already planned separately, and the larger of the
|
||
two jobs — a PowerPoint needs a renderer, not just a viewer.
|
||
2. **A worksheets list.** Not a file manager: one Wonder Ink tab, showing
|
||
the PDFs already in `~/Downloads/Wonder Ink`, each opening in a tab the
|
||
same way a fresh download does. It reads one directory, offers no
|
||
navigation out of it, and needs no new permission — the paths it offers
|
||
are ones the shell would open anyway. That covers "a teacher closed the
|
||
worksheet tab and wants it back", which is the next thing that will be
|
||
asked for.
|
||
|
||
Neither is in this release.
|
||
|
||
### Where Wonder Ink's downloads end, and Chrome's begin
|
||
|
||
They never meet, and not because anything inspects a filename.
|
||
|
||
The download handler is connected to **Wonder Ink's own profile**, so it
|
||
only ever sees downloads that started in a Wonder Ink tab. Chrome is a
|
||
separate application with a separate profile and its own download
|
||
handling, and nothing here changes, watches or intercepts it. There is no
|
||
directory watcher anywhere in the classroom: a PDF that Chrome downloads
|
||
is Chrome's business and stays that way.
|
||
|
||
`~/Downloads/Wonder Ink` is a directory of its own inside the ordinary
|
||
Downloads folder. That is partly tidiness and mostly the security
|
||
argument: it is the only place a worksheet can come from.
|
||
|
||
## What stayed the same
|
||
|
||
* **A separate profile from the ordinary browser.** A teacher's Wonder Ink
|
||
sign-in and the room's shared YouTube account are different sessions in
|
||
different storage that cannot reach each other. The browser is still
|
||
Chrome and nothing in this change touches it.
|
||
* **Cookies live only as long as the window.** This is what the Chrome
|
||
policy's `CookiesSessionOnlyForUrls` did for wonderink.org, and it is
|
||
the reason closing Wonder Ink ends the teacher's session rather than
|
||
leaving the next person signed in as the last one. The whole profile
|
||
rather than one domain now, because this profile only ever goes where a
|
||
lesson sends it. If a room ever wants sign-ins to survive, it is the one
|
||
line `setPersistentCookiesPolicy` in `classroom-wonderink`.
|
||
* **The old Chrome profile is left where it is**, at
|
||
`~/.local/share/ekklesia-classroom/wonderink`. The new one is
|
||
`wonderink-shell` beside it, so rolling back to 0.2.0 finds the old one
|
||
intact.
|
||
* **One scale for the room.** Chrome has to be handed `UI_SCALE` because
|
||
it was measured not to follow the compositor on Wayland. Qt does follow
|
||
it — measured at `UI_SCALE=2.0`, where a Qt client on that session
|
||
reports `devicePixelRatio 2.0` without being told anything — so Wonder
|
||
Ink is deliberately *not* told, or the scale would be applied twice.
|
||
|
||
## Turning it off
|
||
|
||
One future device is a laptop that will not have Wonder Ink at all.
|
||
|
||
WONDERINK_ENABLED=false
|
||
|
||
in `/etc/ekklesia-classroom/device.conf`, then
|
||
`sudo classroom-desktop-layout` and a logout. The launcher comes off the
|
||
panel, the command refuses to start and says why, and nothing Wonder Ink
|
||
owns runs. The browser, Parent Call and the disc player are untouched, and
|
||
the panel stays balanced because the applications are centred rather than
|
||
placed.
|
||
|
||
The setting is read the same way the disc player's line is written: one
|
||
marked line in the packaged layout, edited by `classroom-desktop-layout`.
|
||
Absent, it means yes — every room that has never heard of the setting is a
|
||
room with Wonder Ink.
|
||
|
||
## Present view video
|
||
|
||
**Fixed in 0.2.1.** The investigation below is kept because it is what
|
||
found the cause, and because the second and third candidates in it were
|
||
never ruled out and may still matter.
|
||
|
||
Candidate 1 — the autoplay gesture chain — is the one the new shell can
|
||
act on, and does. Chrome refuses to begin audible playback that no click
|
||
asked for, and in Chrome the only lever was `AutoplayAllowlist` on the
|
||
policy that governs the ordinary browser as well; allowing autoplay
|
||
everywhere on a classroom machine was a real change and was deliberately
|
||
not made on a guess.
|
||
|
||
Wonder Ink is no longer governed by that policy. Its profile is Wonder
|
||
Ink and nothing else, so `PlaybackRequiresUserGesture` is turned off for
|
||
that profile alone and reaches no other page in the room. The ordinary
|
||
browser is a separate application with a separate policy and is not
|
||
touched.
|
||
|
||
Measured both ways in the lab, with a page that starts an *unmuted* video
|
||
from a script with no user gesture — which is what Present view does:
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| the setting at Chrome's default | `AUTOPLAY BLOCKED: NotAllowedError` — the exact error predicted below |
|
||
| the setting as shipped | `AUTOPLAY OK muted=false`, video playing |
|
||
|
||
That is the lab reproducing the failure and then not reproducing it. It
|
||
is **not** confirmation that Present view itself is fixed, because the
|
||
lab has no Wonder Ink account: if Present view still stops after this,
|
||
candidates 2 and 3 below are where to look next.
|
||
|
||
### The observation
|
||
|
||
On the classroom endpoint:
|
||
|
||
- Vimeo videos embedded in Wonder Ink **play correctly** in the normal
|
||
lesson view.
|
||
- The **same** videos in Wonder Ink's **Present** view stop immediately
|
||
when Play is pressed.
|
||
|
||
### What this rules out
|
||
|
||
The system's video capability is fine. The same video, the same host,
|
||
the same browser and the same codecs work a click earlier in the same
|
||
session. Nothing is missing from the machine.
|
||
|
||
**No codecs or media packages were installed for this, and none should
|
||
be.** Installing packages to fix a problem that is not a missing package
|
||
would leave the classroom carrying software it does not need, and would
|
||
not fix Present view.
|
||
|
||
Nothing in the system was changed for this.
|
||
|
||
### The decisive test
|
||
|
||
Run it once at the endpoint. This separates "Wonder Ink" from "the
|
||
classroom's Chrome configuration" in one step:
|
||
|
||
```
|
||
sudo mv /etc/opt/chrome/policies /etc/opt/chrome/policies.off
|
||
google-chrome --user-data-dir=/tmp/plain-chrome
|
||
```
|
||
|
||
That is a stock Chrome with a clean profile and no classroom policy at
|
||
all. Sign in, open the lesson, press Present, press Play.
|
||
|
||
- **Still fails** — it is Wonder Ink, and nothing here can fix it.
|
||
Report it to them with the console output below.
|
||
- **Works** — something in the classroom configuration is responsible,
|
||
and the next section is where to look.
|
||
|
||
Afterwards:
|
||
|
||
```
|
||
sudo mv /etc/opt/chrome/policies.off /etc/opt/chrome/policies
|
||
rm -rf /tmp/plain-chrome
|
||
```
|
||
|
||
### If it turns out to be this end
|
||
|
||
Two candidates, in order of likelihood. **Neither has been applied.**
|
||
|
||
#### 1. The autoplay gesture chain
|
||
|
||
Chrome refuses to start audible playback without a user gesture. If
|
||
Present view starts the player programmatically after its own view
|
||
transition, the gesture from the Present click may not carry, and
|
||
Chrome rejects the play. That would look exactly like this: fine when
|
||
you click the player directly, dead when the view starts it for you.
|
||
|
||
This is stock Chrome behaviour, not a classroom setting — a plain Chrome
|
||
on any machine would do the same, which is what the decisive test above
|
||
checks.
|
||
|
||
Diagnostic: open DevTools (F12) in Present view and press Play. A
|
||
rejected autoplay logs:
|
||
|
||
```
|
||
NotAllowedError: play() failed because the user didn't interact
|
||
with the document first.
|
||
```
|
||
|
||
If that appears, the fix is a scoped policy, not a global one:
|
||
|
||
```json
|
||
"AutoplayAllowlist": ["https://app.wonderink.org"]
|
||
```
|
||
|
||
`AutoplayAllowlist` is recognised by Chrome 152 — verified against the
|
||
installed binary. It has deliberately **not** been added, because
|
||
allowing autoplay everywhere on a classroom machine is a real change and
|
||
should follow evidence, not a guess.
|
||
|
||
*Since 0.2.1 this no longer applies to Wonder Ink, which is not a Chrome
|
||
window any more and allows its own lesson to start a video without
|
||
changing anything about the browser. See the top of this section. It
|
||
still applies to the ordinary browser, where nothing has changed.*
|
||
|
||
#### 2. A popup window that sway tiles
|
||
|
||
sway tiles new windows rather than floating them. If Present view opens
|
||
a second browser window, it gets a tiled geometry the site did not
|
||
expect, and a player that is resized or re-parented mid-start can stop.
|
||
|
||
Diagnostic, while Present view is open:
|
||
|
||
```
|
||
swaymsg -t get_tree | grep -c google-chrome
|
||
```
|
||
|
||
More than one Chrome window means this is worth pursuing.
|
||
|
||
#### 3. Not the classroom policies
|
||
|
||
For completeness, so nobody re-audits them: the managed policy in
|
||
`/etc/opt/chrome/policies/managed/` sets first-run, sign-in, password
|
||
and homepage behaviour only. None of it touches media, autoplay,
|
||
iframes, fullscreen or site permissions.
|