feat: give Wonder Ink tabs, and let a device say it has no Wonder Ink

Teachers prepare a lesson across several tabs — the lesson, its video,
the activity — and app mode is the one thing that cannot give them that.

Chrome cannot be asked for "tabs but no omnibox". App mode has no tabs; a
normal window has an address bar and no policy, flag or extension removes
it; tabbed web apps need 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 ours, and the
engine is Chromium either way.

QtWebEngine, from Debian, measured before it was chosen rather than
after. The usual and correct objection is codecs: on Debian 13 it reports
H.264, AAC, MP3, VP8 and VP9 all playable, MediaSource supports avc1,
mp4a and vp9, and a 720p H.264 file decoded and played to the end in the
lab. app.wonderink.org loads, and its sign-in is an ordinary email and
password form rather than a Google flow that would mind an embedded
browser. The user agent has the QtWebEngine token stripped so a site
gating on it sees the Chromium this actually is, and Widevine is handed
over from Chrome's own bundle when the machine has Chrome, which a
classroom always does.

    [ Lesson ] [ Video ] [ Activity ]

The strip appears only above one tab, so an ordinary lesson looks exactly
like the application it replaces. A tab can only come from the lesson —
target=_blank, window.open, a middle click, a Ctrl+click — and there is
no address bar, no search and no button that makes a blank tab. No
allowlist of domains either, on purpose: a list would have to be right
about every video service a lesson might ever embed.

Three bugs the lab caught and this fixes. A lesson calling window.print()
did nothing at all, which is worse than no print button because a teacher
concludes the printer is broken; printRequested is now bound to the page
that asked. Leaving fullscreen left the window 200 pixels wide in the
corner — a window that has only ever been maximised has no ordinary
geometry to return to — so the fullscreen bit is cleared rather than
maximised being re-requested. And the Present view video failure written
up in docs/wonder-ink.md is acted on at last: that investigation named
the autoplay gesture chain as the likeliest cause and declined to fix it,
because in Chrome the only lever governed the ordinary browser as well.
Here it does not — this profile is Wonder Ink and nothing else — so a
lesson may start its own video and no other page in the room may.
Reproduced both ways in the lab with an unmuted video started by a script
with no gesture: NotAllowedError at Chrome's default, playing as shipped.
That is not proof Present view is fixed; the lab has no Wonder Ink
account, and candidates 2 and 3 in that document are still open.

Measured on a rebooted machine, from the package: tabs open from a link
and from window.open, switch, and close; closing one leaves the others;
closing the last closes Wonder Ink; H.264 video plays and fills the
display in fullscreen and comes back maximised with its tabs; the print
dialog offers the room's own Elmer queue; a download lands in ~/Downloads;
back and forward work from a right-click menu that is built rather than
filtered; pressing the panel button twice raises one window rather than
starting a second on the same profile; KWin reports the window as
ekklesia-wonderink, which is one panel button and not two.

Separate from the browser as before, in its own profile, with cookies
that live only as long as the window — what the Chrome policy did for
wonderink.org, and the reason closing Wonder Ink ends a teacher's
session. Chrome's old profile directory is left untouched beside it, and
the browser's own policy is not edited by any of this.

Wonder Ink is now optional per device, through WONDERINK_ENABLED in
device.conf, because one future device is a laptop that will not have it.
It rides the mechanism the disc player already uses: one marked line in
the packaged layout, edited by classroom-desktop-layout. False takes the
launcher off the panel and makes the command refuse to start; the
browser, Parent Call and the disc player are untouched and the panel
stays balanced. Absent means yes, so every existing room is unchanged.

One scale, still. Chrome has to be handed UI_SCALE because it was
measured not to follow the compositor; Qt does follow it — devicePixelRatio
2.0 at UI_SCALE=2.0, measured — so Wonder Ink is deliberately not told,
or the room would get it twice.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-19 21:29:17 -05:00
co-authored by Claude Opus 5
parent 23dff0b7b4
commit de5025ad22
23 changed files with 1049 additions and 102 deletions
+162 -8
View File
@@ -1,6 +1,155 @@
# Wonder Ink: Present view video
# Wonder Ink
## The observation
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.
## 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:
@@ -9,7 +158,7 @@ On the classroom endpoint:
- The **same** videos in Wonder Ink's **Present** view stop immediately
when Play is pressed.
## What this rules out
### 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
@@ -22,7 +171,7 @@ not fix Present view.
Nothing in the system was changed for this.
## The decisive test
### The decisive test
Run it once at the endpoint. This separates "Wonder Ink" from "the
classroom's Chrome configuration" in one step:
@@ -47,11 +196,11 @@ sudo mv /etc/opt/chrome/policies.off /etc/opt/chrome/policies
rm -rf /tmp/plain-chrome
```
## If it turns out to be this end
### If it turns out to be this end
Two candidates, in order of likelihood. **Neither has been applied.**
### 1. The autoplay gesture chain
#### 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
@@ -82,7 +231,12 @@ 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.
### 2. A popup window that sway tiles
*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
@@ -96,7 +250,7 @@ swaymsg -t get_tree | grep -c google-chrome
More than one Chrome window means this is worth pursuing.
### 3. Not the classroom policies
#### 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