Files
ekklesiaandClaude Opus 5.5 fd4330a649 docs: temporary workspace restore, the admin link bubble, and Sunday's checks
wonder-ink.md: where WonderInk keeps its sign-in (Basic auth in a cookie)
and why only tabs are restored; what comes back and what never does; what
counts as use; when it is not restored; the test-only override. And the
link bubble teachers saw: WonderInk's CKEditor 5 for Admin accounts,
where a plain click on a link is an editing click by design; Viewers get
one-click links, measured on the real Saul lesson, and Wonder Ink does
not override the editor.

The manual says closing still signs you out and recent tabs come back
for two hours. testing.md has the Little Waders checklist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 21:12:17 -05:00

44 KiB
Raw Permalink Blame History

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 image when there is a picture under the pointer, print page.

A link in a lesson opens with one click in a new tab, and Wonder Ink shows that tab; the lesson stays in its own. That is what the site asks for — every link in a lesson's content carries target="_blank" — and what the engine does with it (createWindow, answered with a tab that takes the focus). Measured on the real site, on the real Rise and Fall of Saul lesson: "Saul Welcome Question Slide" is <a target="_blank" href="…/SaulWelcomeQuestionSlide.png">, and one click opened the slide in a new tab, at 100%, at 200% and in Full Screen.

The URL bubble teachers saw is WonderInk's editor, shown to admins. Testing 0.2.6.3 found that clicking such a link showed a small bubble with the address and Edit / Remove instead of following it. Found by reading the site's code and confirmed with the account roles:

  • the site shows lesson text in CKEditor 5 (35.2.1, its RTE_USE_CKEDITOR_PRO component). For a Viewer the content is read-only and a link is a link; for an Admin it is editable, in place;
  • in an editable CKEditor 5, a plain click on a link is an editing click by design (LinkEditing._enableLinkOpen opens a link only with Ctrl, ⌘ on a Mac); the editor answers it with its link balloon — the address, Edit link, Unlink. It is the same code in Chrome;
  • moving the teacher's account to Viewer made the bubble go away.

So Wonder Ink does not override it. Doing so would mean catching clicks inside WonderInk's editor, which is how admins edit lessons, to fix something the account's role already decides. An admin who wants to follow a link straight away: Ctrl+click it.

What each kind of link does, as tools/lab/wonderink-print-check links:* clicks them on a signed-in lesson page:

a text link, a linked picture, another site's link (target="_blank") a new tab, shown
a worksheet PDF a new tab in the PDF viewer; Print fetches it and prints it as a worksheet
a picture a new tab; Print lays it out as a picture, and right-click offers Print image
a lesson button that opens a window a new tab, shown
a lesson button, an in-page anchor (#…) what the page does; no tab
a link with no target followed in the same tab, as in a browser; Back returns

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.

Printing has no Print to File and no file chooser of any kind: the preview (below) offers the room's CUPS printers and nothing else.

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.

Printing

The crash (0.2.2, Little Waders)

Wonder Ink closed completely every time a teacher confirmed a print job: SIGSEGV in QPrinter::resolution(), twice, both through QtWebEngine's print-completion path.

QWebEngineView.print(printer) is asynchronous. In Qt 6.8.2 (qwebengineview.cpp) it stores the raw QPrinter* and returns; when Chromium has rendered the page, didPrintPage() calls currentPrinter->resolution() and hands the printer to a worker thread. 0.2.2 made the printer a local variable in print_view(), so PyQt deleted it when the function returned and Qt read freed memory. Reproduced in the lab with the 0.2.2 code: QPrinter::resolution() ← QWebEnginePagePrivate::didPrintPage ← WebContentsAdapter::didPrintPage, the Little Waders backtrace, and no job in CUPS.

Keeping the printer alive until printFinished stops the crash — that was built and measured first — and it is still not a working print path:

Measured, QtWebEngine 6.8.2, lab queue built from Elmer's PPD Qt view.print() printToPdf() + lp
A three-page lesson 24.6 MB of 300 dpi pictures, no text, A4 on a test queue 18 KB vector PDF with its text, on Letter, 0.43 s through Elmer's filters
A worksheet PDF tab one blank page the worksheet file itself
A PDF a site shows inline — printToPdf never answers (CreatePreviewDocument got 0 page count) — so it is downloaded and printed as a file
A queue refusing jobs reported as printed "Refusing did not take the job: Destination … is not accepting jobs."
Black and white on Elmer — IPP print-color-mode is ignored by the generic PCL 6 driver ("Unknown option", prints colour); ColorModel=Grayscale gives pxlmono

Qt's path also mishandles three things its own source makes plain: a second print() on a page that is still printing is refused with no printFinished, the worker keeps the printer after the tab has gone, and quitting destroys the printer under that worker.

So 0.2.6.1 does not use Qt printing at all. Print makes a PDF — a worksheet already is one; a lesson page is rendered by the engine onto the printer's own paper with Chrome-like 10 mm margins — shows it, and hands it to CUPS with lp, which is how Chrome prints in the same rooms. Nothing asynchronous ever holds a printer, so closing a tab or Wonder Ink cannot break a job: once lp returns, the job is CUPS's.

The preview

One dialog, for lessons and worksheets alike:

  • the document as it will print, a whole page at a time, ‹ › to turn;
  • Printer — the CUPS destinations, the system default preselected (Elmer, in every room);
  • Pages — all, or some: 2, 1-3, 1, 3, 5-6, checked against the document with the reason given if wrong;
  • Copies — 1 to 50;
  • Color — colour or black and white, only where the printer's driver has a grey mode, and asked for in that driver's own terms. Since 0.2.6.3 the preview turns grey too (below);
  • Cancel sends nothing; Print returns to the lesson once CUPS has the job, and a notification says where it went. A refused job keeps the preview open with CUPS's reason.

Choosing a printer with different paper lays a lesson page out again. A page that closes its own tab while the preview is open is reported, not printed. Print pressed again while a preview is open does nothing, and so does a page calling window.print() meanwhile. The PDF viewer's own print button opens the same preview.

Pictures (0.2.6.2)

A picture a lesson opens in a tab of its own — a colouring page as a PNG or JPEG — printed in 0.2.6.1 with black bars above and below it. Measured: the bars were neither in the picture nor in the page layout. The engine shows a lone picture on a viewer page whose background is rgb(14, 14, 14), scaled to the window, and Qt prints page backgrounds by default; the dark page went onto the paper around the artwork.

So a picture is no longer printed as that page. The preview takes the picture itself — drawn onto white, which is what transparent parts are on paper, at up to 3300 pixels (300 dpi across Letter) — and lays it out on the printer's own paper, and the teacher chooses:

  • Size — as large as fits, 75%, 50%, 33% or 25% of that;
  • Position — nine squares, like the page itself;
  • Paper — automatic (landscape for a wide picture), upright or sideways;
  • Fill the whole page — covers the printable area and says it cuts the edges off. Nothing is cropped unless this is ticked.

The preview is the PDF that will print, laid out again after every change. tools/lab/wonderink-print-check measures it on the file CUPS actually kept (PreserveJobFiles), which is byte-for-byte the preview's: the picture within 1 point of where it was asked for, the paper turned as asked, and no ink anywhere else on the page. Worksheet PDFs and lesson pages have no picture controls; nothing about them changed.

Page backgrounds are also no longer printed for lesson pages, as in Chrome, where "Background graphics" is off unless asked for. A white-on- black page prints dark grey on white: Blink darkens light text when the background is left off, measured.

Temporary PDFs live in the teacher's $XDG_RUNTIME_DIR and are removed when the preview closes.

Black and white is shown as it prints (0.2.6.3)

Printing through Elmer found that Black and white came out grey on paper while the preview stayed in colour. 0.2.6.2 only told the driver: the preview was the colour PDF, CUPS was given the colour PDF, and ColorModel=Grayscale turned it grey inside Elmer's filters.

So black and white is now made before the preview, not after it. When it is chosen, the preview makes a grey copy of the document with Ghostscript, shows that copy, and Print hands CUPS that same file — still with ColorModel=Grayscale, so Elmer's generic PCL 6 driver prints it with pxlmono as before rather than as a colour job that happens to be grey. Choosing Color shows and prints the colour document again.

Measured before building it (Ghostscript 10.05.1, in the lab):

a lesson page, a picture page, a leader guide, a fillable form grey and only grey; same pages, page sizes and words; text still text; a filled-in form field and a drawn mark kept, in grey
against what Elmer would have printed Ghostscript's grey rendering of the colour original and of the grey copy give the same greys: within 1 level in 255 on every filled area and picture. Small text can land a fraction of a pixel over — on a real lesson page, 12 of 32,754 8×8 blocks, all on one line of body text, in pairs of opposite sign. Elmer's driver is Ghostscript. (poppler's own grey differs on 7% of a coloured page, which is why the copy is not made with poppler)
how long 0.05 s for a worksheet, 0.2 s for a text lesson page, 1.3 s for a full-page photograph, 2 s for 42 pages — and 4.4 s for one lesson page with eleven full-size pictures, under the real Plasma session. -dUseFastColor saves 40% and changes the greys, so it is not used
a PDF Ghostscript cannot read exits 1 and still leaves a file behind

From those:

  • the copy is made in the background, the preview says Preparing black and white… and Print waits for it — four seconds is long enough to look like a frozen window. It has a minute before it is given up on, not the 20 s a page render has: a lesson of photographs on a slower laptop is slow, not stuck. Cancel stops it at once;
  • it is made once per document and kept, and each drawn page is kept, so turning Color and Black and white back and forth is instant after the first time, and stays on the page the teacher was looking at;
  • a conversion is judged by its exit status and its page count, never by whether a file appeared. If it fails the preview says so and offers Color. It never prints the colour file in its place: the teacher asked for black and white;
  • pages are never turned (pdfwrite otherwise rotates a page to match its text), and annotations are drawn into the page as they print, so no later filter can draw them again in colour;
  • a printer whose driver has no grey mode is not offered black and white, and choosing one returns the preview to colour.

tools/lab/wonderink-print-check shows a colourful lesson in colour, turns it to black and white and back five times on page 2, and prints page 2 twice: every grey preview is grey pixel for pixel, every colour one is not, the grey copy is made once, and the job CUPS kept is byte-for-byte the file the preview showed, grey on every page, sent with ColorModel=Grayscale through pxlmono. The same for a worksheet, a 50% top-left picture, a return to Color (which prints in colour through pxlcolor), and Cancel.

Print image: a picture from inside a lesson (0.2.6.3)

Right-clicking a picture in a lesson and choosing Print printed the whole page around it. Some of Wonder Ink's pictures cannot be opened on their own through the site at all, so a teacher had no way to give that picture the picture layout 0.2.6.2 introduced.

The right-click menu now says what it will print. It asks the engine's own hit test what is under the pointer:

Under the pointer Measured mediaType The menu offers
a picture (<img>, also inside a link, a frame, from blob: or data:) Image Print image… and Print page…
a <canvas> Canvas Print image… and Print page…
words, empty space, a CSS background picture None Print page…

Print page is always there. Ctrl+P and the Print button print the page, as before.

How the picture is taken was measured before anything was built, on a lab site with each kind of picture a lesson might have, a real right-click on each, and the site logging every request:

signed-in picture hotlink-protected one-time address blob: / data: another site's canvas WebP
engine's "download image" ✓ ✓ 404 — fetched again ✓ ✓ nothing a file Qt cannot read
page.download(url) ✓ 403 — no Referer 404 – ✓ – the same
fetch() from the page ✓ ✓ ✓ – refused (CORS) – the same
draw it on a canvas ✓ ✓ ✓ ✓ refused (tainted) – ✓
engine's "copy image" ✓ ✓ ✓ ✓ ✓ ✓ ✓

Only the engine's own copy image hands over the picture in every case, and it is what Wonder Ink uses. It is the bitmap the page has already decoded — full size, transparency kept, any format Chromium reads (Qt itself cannot read WebP here) — and it fetches nothing: the site saw no request at all. So a picture behind the teacher's sign-in needs no sign-in again, and a picture the site hands out once is still there. 0.01 to 0.05 s in the lab.

It goes through the clipboard, which is the teacher's. So what is on it is kept first and put back as soon as the picture arrives — whenever it arrives, even after Cancel. Measured under the real Plasma session (KWin, Wayland) with another program owning the clipboard: saved in 0.002 s, the picture in 0.016 s, and that other program then pasted its own text again.

The picture then goes into exactly the 0.2.6.2 picture preview — drawn on white, capped at 300 dpi across Letter, Size, Position, Paper, Fill the whole page, Color — named Picture from the lesson's title.

When it cannot be taken. A picture that never loaded is still a picture to the hit test, and the engine then copies nothing. After three seconds the preview says This picture could not be taken from the page and that nothing was printed, with Print off. It does not fall back to printing the page; Print page is one right-click away and says what it does.

What it cannot do, and says nothing about because it cannot know:

  • a CSS background picture is not a picture to the engine's hit test — Chrome's own menu has no "Save image" for one either. The menu offers only Print page. If Wonder Ink's site draws its artwork this way, that is the next thing to look at; nothing in the lab says it does;
  • a video frame is not offered;
  • the teacher's clipboard is put back into Wonder Ink. Under Wayland with no clipboard manager — this session has none — the clipboard belongs to whichever program last set it. After a Print image that is Wonder Ink, so text copied in another program before it can still be pasted while Wonder Ink is open, and is gone once Wonder Ink closes. Measured in the lab session. Leaving the picture there instead would lose it at once.

tools/lab/wonderink-print-check runs a signed-in lesson site (tools/lab/wonderink-lesson-site.py: session cookie, one-time addresses, a second origin) and right-clicks for real: the words (Print page only, and the page prints), a JPEG, a PNG, a transparent PNG, a one-time picture, another site's, a blob:, a WebP, a canvas, a picture inside a link, a JPEG at 50% top left in black and white, a picture that never loaded, and Cancel. Each is judged on the job CUPS kept: where and how large, no ink elsewhere, the corner mark there — and no request to the site while it printed, and the teacher's clipboard theirs again.

tools/lab/wonderink-print-check runs all of this against the installed command in the lab, and judges every scenario by what CUPS actually received. tests/test-classroom-printing.sh covers the CUPS side at every build. Neither is paper. The first print through the real Elmer is a job for somebody standing next to it.

Any other error

An exception escaping any Qt handler makes PyQt6 abort the whole process — measured: a mistake in one slot and the window vanishes. Wonder Ink now logs it to the journal and carries on.

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, as Chrome's CookiesSessionOnlyForUrls did for wonderink.org, so closing Wonder Ink signs the teacher out and the next person is not signed in as the last one. WonderInk's sign-in is the teacher's password in a cookie (measured, 0.2.6.4), which is one more reason it never reaches the disk. What does survive a close, for two hours, is the list of tabs — see Temporary workspace restore, below.
  • 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 — see How large a lesson is, below.

Temporary workspace restore (0.2.6.4)

A teacher who closes Wonder Ink by accident, or whose computer crashes or restarts, gets their recent tabs back if they open it again within two hours of last using it. After that, Wonder Ink starts fresh and the tabs are forgotten: the next teacher — overnight, next week — never meets the last one's. It is not a sign-in and not a memory of tabs.

10:00  using Wonder Ink
10:45  closed by accident          10:50  opened again: the tabs are back
11:00  finished, closed            next Sunday: a fresh Wonder Ink

Why it is tabs and not the sign-in

The first design kept the sign-in. It was measured on the real site before it was built, and stopped:

rrf-auth a cookie the site's JavaScript writes: Basic and the base64 of the teacher's email and password, sent as a header on every request. Not a token the server issued
rrf-session-id, rrf_tsec_tok, auth session cookies alongside it; none survives the window, which is why closing Wonder Ink signs the teacher out
Local Storage mddui_local_storage, Session Storage screen variables and a screen cache
the site's start-up asks the server to authenticate again every time it loads; a reload in the lab ended signed out even with the cookie present

Keeping the sign-in would have meant keeping the teacher's password, in a form anyone at the maintenance terminal could reverse, on a shared computer. It is not done. Closing Wonder Ink still signs the teacher out, exactly as before; the engine writes no cookie to disk for any site.

What comes back

  • Resource tabs — a slide, a picture, a worksheet or video the lesson linked to — exactly as they were. WonderInk's files are public (the lesson's slide loads with no sign-in, measured), so they need none.
  • WonderInk's own tabs as one. Every WonderInk page has the same address (https://app.wonderink.org/?) whichever lesson it shows — the site draws each screen itself — so a restored WonderInk tab opens the site's start page and asks the teacher to sign in, as it would anyway. Several of them become one, where the first was, so a teacher meets one sign-in page, not five.
  • Worksheet tabs, named and where they were — if the file is still in ~/Downloads/Wonder Ink and still a PDF. A worksheet that has gone is left out, silently. Nothing outside that folder is ever opened from a kept workspace, whatever the file says.
  • The order, and the tab that was in front. The tab in front loads at once; the others load the first time they are clicked. Measured: a PDF loaded in a tab nobody was looking at was fitted to a window of no size, and showed at 10% when clicked. Until then a tab is kept by the address it will load, so closing Wonder Ink again loses nothing.

Measured on the real site, with a real Viewer account, under the lab's Plasma session: the Rise and Fall of Saul lesson, its welcome slide and its WONDER@HOME PDF, the slide in front; closed and reopened — the three tabs in that order, the slide in front, the WonderInk tab at its sign-in page; and again after rebooting the machine with Wonder Ink open. The kept file held the three addresses and titles, and none of the password, the Basic-auth value or the email address.

Never: the sign-in, a cookie, a header, a page's storage, a form, the clipboard, a print preview or any of its temporary files.

What is kept, and when

~/.local/share/ekklesia-classroom/wonderink-workspace.json, readable only by the classroom account:

format, last_activity, active, attempts,
tabs: [{kind: "web" | "worksheet", where: address or file, title}]

An address is kept only if it is http or https and carries no credential — no user and password in it, no query parameter named like a token, key, password, session or signature. tests/ writes a workspace with such addresses and a Basic-auth value and checks the file holds none of them; the lab signs in on a stand-in whose sign-in is Basic auth, as WonderInk's is, and checks the same.

It is written a second after the tabs change — opened, closed, dragged into another order, brought to the front, navigated — and the time of last use at most every 30 seconds, whole, to a temporary file renamed into place. A crash or a power cut loses at most half a minute of use and never leaves half a file.

What counts as using it

The teacher's input reaching Wonder Ink: a click, a key, the wheel, a touch — so following a link, scrolling, switching, opening and closing tabs, opening a worksheet, printing and zooming all count. Nothing a page does by itself does: timers, animation, background requests, analytics, advertising, video buffering, service workers — measured with a page making a request every second and animating for 20 s: not activity. A print preview left open untouched is not activity either.

Opening Wonder Ink and having the tabs restored is not using it. The time kept is the teacher's last use, carried over, so closing and reopening cannot keep a workspace alive by itself; the teacher clicking in the restored tabs is using it, and the two hours start again.

When it is not restored

At start, and only then: more than two hours since the last use, a file that cannot be read or is from another version, a clock gone backwards by more than a minute — Wonder Ink starts fresh, removes the file and says why in the journal. No question is asked.

A restore that crashes Wonder Ink does not make it unusable. Starting from a workspace is counted in the file before any tab loads, and cleared once Wonder Ink has run 20 seconds. After two starts that did not get that far, the workspace is abandoned and Wonder Ink starts fresh, with a line in the journal.

Two hours is IDLE_LIMIT_SECONDS in lib/classroom_wonderink_workspace.py, the one place it is decided.

Testing without waiting two hours. From the maintenance terminal in the session (Meta+Ctrl+Alt+Shift+K), with Wonder Ink closed:

CLASSROOM_WONDERINK_TEST_WORKSPACE_SECONDS=120 classroom-wonderink

That one Wonder Ink keeps the workspace for two minutes after the last use, and says TEST ONLY in the journal. The variable can only shorten the time, nothing in the classroom sets it (a test checks), and it lasts as long as that process.

What it cannot do

  • Reopen a lesson where it was. WonderInk's screens have no address of their own, so its tab comes back at its start page; after signing in the teacher opens the lesson again. The slides, pictures and worksheets they had open alongside it are already there.
  • Know who is at the keyboard. Two hours is two hours of nobody using Wonder Ink. A teacher who leaves and whose room is used within two hours leaves their tabs — not their sign-in — to the next person.

tools/lab/wonderink-workspace-check runs each of these against the installed command: restored after 5 and 90 minutes; fresh after two hours and a minute, the next day and the next week; reopened twice untouched; activity after a restore; a busy page; an untouched print preview; pressing Print; a closed tab; tabs reordered; a worksheet, and one whose file has gone; killed outright; a restore that crashes twice; a corrupt file, a later format, a half-written file; the privacy of the file; and restored tabs printing. tests/test-classroom-wonderink-workspace.sh covers the rules at every build.

How large a lesson is

Teachers found lessons in Wonder Ink too small next to the same site in the ordinary browser. Measured in the lab (Plasma 6 on KWin, Debian 13) by asking each page for window.devicePixelRatio and its width in CSS pixels, with the display scale set the way a room sets it (kscreen-doctor and UI_SCALE):

Display scale Wonder Ink 0.2.6 Chrome, as classroom-browser starts it
100% dpr 1, 1920 px wide dpr 1, 1920 px wide
150% dpr 1.5, 1280 px wide dpr 2.25, 854 px wide
200% dpr 2, 960 px wide dpr 4, 500 px wide

Wonder Ink was right by the desktop's measure: it follows the display scale exactly, like the panel and every dialog. Chrome is scaled twice. Under Plasma it follows the compositor's scale itself, and classroom-browser also forces --force-device-scale-factor=UI_SCALE — a flag from the sway session, where Chrome was measured not following the compositor. So in any room above 100%, every page in the browser is UI_SCALE times larger than the desktop around it, and that is the size teachers read lessons at.

0.2.6.1 does not change the browser — it is the size the rooms know, and changing it everywhere is its own decision. Wonder Ink's pages start at the browser's size instead: page zoom = UI_SCALE, the room's own clamped number, which is exactly 1.0 in a room at 100%. Wonder Ink's window, tabs and dialogs are not zoomed; Qt already scales them once, correctly. Measured after the change: at 150% the lesson reports dpr 2.25 and 853 px, Chrome dpr 2.25 and 854 px.

The teacher's own size goes on top: Ctrl+Plus (and Ctrl+=, and the keypad's), Ctrl+Minus, Ctrl+0 for normal, Ctrl+wheel, or the right-click menu. Chrome's steps, 50% to 300% of the room's normal size, one level for every tab including new ones. It is remembered in ~/.config/ekklesia-classroom/wonderink.conf as a single LEVEL= line — its own file, nowhere near the engine profile, so remembering a size can never mean remembering a sign-in — and relative to the room, so an administrator changing UI_SCALE later still gets the right result.

Measured along the way:

  • the engine keeps a tab's zoom across every navigation, same site or not, so it is set once when the tab is made and never re-applied;
  • zoom does not change what prints: the same lesson is 3 pages at 100%, 200% and 400%;
  • page zoom does not reach a worksheet tab. Chromium's PDF viewer follows the display scale like the window, fits the page to its width, and has its own − / + zoom in its toolbar, which is what a teacher uses there;
  • two Qt actions on one key are ambiguous and Qt does neither — Ctrl+Minus did nothing while it was bound once by name and again as KDE's standard zoom-out key. One action per command now carries all its keys, which also fixed Ctrl+Shift+Tab (bound twice the same way since 0.2.1).

tools/lab/wonderink-zoom-check asks the installed command all of this. The lab's injected keyboard does not reach its nested session, so the check delivers keys through Qt's own key path to the focused page; a physical Ctrl+Plus on a classroom keyboard is a hardware check.

Why lessons load slowly

Measured before anything was changed, and nothing about loading was changed, because the evidence does not point at Wonder Ink.

In the lab, on the same connection, the same page, timed by each engine's own Navigation Timing until the sign-in form exists (the site is a 3.7 KB shell that builds itself in JavaScript, so its load event is not the moment a teacher can use it):

Until the sign-in page is usable Wonder Ink Chrome
cold cache, 3 runs 2.86 – 3.00 s 2.79 – 3.48 s
warm cache, 3 runs 2.20 – 2.49 s 1.96 – 2.15 s
window on screen after launch 0.33 s 0.42 s
  • The site is the weight: 119 – 160 requests and 3.2 MB for a sign-in form, about 430 KB warm. Its console says it loads the Google Maps JavaScript API twice, synchronously. That is Wonder Ink's to fix.
  • Wonder Ink is about a quarter of a second slower warm: it revalidates the page shell (62 – 87 ms to first byte) where Chrome serves it from its cache. Not the "very long time" teachers describe.
  • Memory: about 610 MB for one tab and 115 MB more for each further tab — eight tabs, 1.4 GB — one renderer each. Idle CPU about 1%: nothing spins in the background.
  • The network: from the lab, 0.14 – 0.22 s to first byte; from the development host, 0.42 – 0.44 s. Cloudflare in front, uncached.

What the lab cannot show is the machines: it draws in software for both engines. A classroom whose graphics fail in QtWebEngine but work in Chrome would look exactly like the complaint. So there is a command for a technician, run from the maintenance terminal inside the teacher's session (Meta+Ctrl+Alt+Shift+K):

classroom-wonderink --diagnose

It prints the engine's versions, the scale, what WebGL reports as the renderer — a real driver's name, or "none … drawing in software" — and times the sign-in page from a cold cache in a throwaway profile, beside a running Wonder Ink without touching it. Compare its time with the lab's 2.9 s, and its renderer with Chrome's on the same machine (open chrome://gpu there).

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:

"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.

Later

What testing of 0.2.6.1 asked for next is in backlog.md.