Commit Graph
100 Commits
Author SHA1 Message Date
ekklesiaandClaude Opus 5.5 cb12e59dea test: the lab prints from a signed-in lesson, and judges black and white on the job
A lesson site for the lab (session cookie, one-time addresses, a second
origin, a request log) and real right-clicks on its words and on a JPEG, a
PNG, a transparent PNG, a one-time picture, another site's, a blob:, a
WebP, a canvas, a linked picture, a broken one, and Cancel. Each is judged
on the job CUPS kept — where and how large, no other ink — and on the site
log: nothing was fetched again. The clipboard is the teacher's afterwards.

Black and white: a colourful lesson turned back and forth five times on
page 2, a worksheet, a picture at 50% top left, a page full of pictures,
back to Color, and Cancel. Every grey preview is grey pixel for pixel and
the job is byte-for-byte the file shown, through pxlmono. The three 0.2.6.2
black-and-white scenarios now wait for the grey file before Print, as a
teacher has to. 343 checks, none failing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 16:34:38 -05:00
ekklesiaandClaude Opus 5.5 594fbd810e fix: black and white is shown as it prints, and right-click prints the picture
Printing through Elmer found two things. Black and white came out grey
while the preview stayed in colour: 0.2.6.2 only told the driver. And
right-clicking a picture in a lesson and choosing Print printed the whole
page around it, where some pictures cannot be opened on their own at all.

Black and white is now made before the preview. Ghostscript makes a grey
copy, the preview shows it, and CUPS is given that same file, still with
ColorModel=Grayscale so Elmer's PCL 6 driver prints with pxlmono. Measured
first: Ghostscript's own grey of the colour original and of the copy give
the same greys (Elmer's driver is Ghostscript); poppler's do not. It takes
up to 4.4 s for a lesson page of pictures, so it runs in the background
with a minute to finish, is made once and kept, and a failure is said so
and never printed in colour instead. Ghostscript is now a named dependency;
cups-filters already brought it.

Right-click asks the engine's hit test: on a picture or a canvas it offers
Print image and Print page, elsewhere only Print page. The picture comes
from the engine's own copy image — the only one of five ways measured that
worked for a signed-in picture, a hotlink-protected one, a one-time
address, blob:, data:, another site's, a canvas and WebP — and it fetches
nothing, so the teacher's sign-in is not needed again. It goes through the
clipboard, which is kept and put back; measured under KWin with another
program owning it. A picture that never arrives is said so; the page is
not printed in its place. It then gets the 0.2.6.2 picture layout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 16:34:38 -05:00
ekklesiaandClaude Opus 5.5 550c0bf126 release: 0.2.6.2, for the canary machine only
Pictures print as the picture — no black bars — at the size and place a
teacher chooses; a presentation that runs out of memory says so and can
be tried again, and the import service survives it; and a notification's
Configure button no longer opens KDE's settings under the teacher account.

Published to canary: ekklesia-classroom_0.2.6.2_amd64.deb, sha256
a7ca958ac8352bb194a2685e4339fe8cafceb5fee871e4c023402c7320945618, gated
on that file as an upgrade from 0.2.6.1 and from 0.2.2. 0.2.6.1 is
still on the server as it was. Stable still answers 0.2.2; nothing here
wrote to it. The laptop is offline and will take this when it is next
on; no desktop is affected.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 07:35:24 -05:00
ekklesiaandClaude Opus 5.5 2325a5993b test: the worksheet check tears its pages down before their profile
Intermittently it segfaulted after every check had passed, leaving
teardown order to Python; a profile released under a live page does
that. Eight runs clean afterwards. The real Wonder Ink, closed six times
by its window's close button in the lab, exited 0 each time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 07:19:45 -05:00
ekklesiaandClaude Opus 5.5 2d1cc016c5 test: the lab installs a package the way a classroom does
from-package used dpkg -i and then apt-get -f install. With 0.2.6.2's
Conflicts: khelpcenter, dpkg refused and -f resolved it by dropping the
classroom package, so the lab ran every check against nothing. The
updater runs apt-get install on the file, which removes the conflict and
installs; measured, and now the lab does the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 07:07:54 -05:00
ekklesiaandClaude Opus 5.5 59cb51c68b docs: Sunday's checks for 0.2.6.2 — pictures, a large deck, the settings route
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 07:01:39 -05:00
ekklesiaandClaude Opus 5.5 0bbe2a1ee0 docs: the 0.3.0 backlog, from testing 0.2.6.1
Wonder Ink's next interface, the PDF viewer's Download and second Print,
readable worksheet names, a tab's sound indicator, the popup that Chrome
does not show, session retention, and the site's weight; Presentations
as a maximisable tile grid with click-to-advance and a hidden pointer;
the launcher's subtitles and Parent Call's flash. Recorded, not built.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 07:01:10 -05:00
ekklesiaandClaude Opus 5.5 cd798a33a1 fix: a notification's Configure button no longer opens system settings
The kernel memory killer's "Memory Shortage Avoided" notice carried a
Configure button that ran kcmshell6 and opened KDE's whole Notifications
settings under the teacher's account. Reproduced in the lab.

Plasma draws that button without consulting the Kiosk policy, but
kcmshell6 and System Settings do: every settings module Debian 13's
Plasma 6.3 installs (90) is now refused in the classroom's immutable
kdeglobals, so the same click shows only "disabled by the system
administrator". The clock's Configure Digital Clock goes with
plasma/allow_configure_when_locked=false; Configure Audio Devices
disappears on its own; the disabled window's Help button opened KDE Help
Center, which the package now conflicts with (plasma-desktop only
recommends it).

The maintenance terminal runs outside the classroom's configuration and
still opens every module, verified. converged-check fails if any module
on a machine is ever left unrestricted: 126 passed in the lab.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 06:59:53 -05:00
ekklesiaandClaude Opus 5.5 3855989029 fix: a presentation that runs out of memory says so, and the service survives it
On a 4 GB VM the kernel killed the import service mid-conversion. Measured:
LibreOffice needs 0.2 GB for a text deck and 1.4-2.1 GB for decks of many
different photographs, for about twenty seconds — a spike, not a leak.
What made it a problem was the rest: OOMPolicy=stop took the watcher down
with LibreOffice, it restarted and resumed the same conversion with no
limit, and the library said "Getting this ready" throughout.

The service is capped at 40% of memory, is first in line when memory runs
short everywhere, and survives a killed LibreOffice (OOMPolicy=continue).
A killed conversion is a failed import of kind "memory"; an import
interrupted twice is not tried a third time; the card says the computer
needed more memory, to close other programs and choose Try again; an
import nobody has worked on for 20 minutes shows the same card; and an
exclusive lock keeps it to one import at a time.

Lab, the real service and LibreOffice: at a 4 GB-machine ceiling the
2.1 GB deck still imported; with no swap and a 300 MB ceiling LibreOffice
was killed, the watcher kept its PID and the card offered Try again,
which worked; the whole service killed during two attempts gave up after
the second; three decks at once never ran two LibreOffices together.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 22:16:50 -05:00
ekklesiaandClaude Opus 5.5 940f63e0e5 fix: a picture prints as the picture, where and as large as the teacher asks
The black bars above and below a printed picture were neither in the
picture nor in the layout. A PNG or JPEG in a tab of its own is shown by
the engine on a viewer page whose background is rgb(14, 14, 14), and Qt
prints page backgrounds by default; 0.2.6.1 printed that page.

A picture is now taken as itself — drawn onto white, capped at 300 dpi
across Letter — and laid out on the printer's paper by the preview, with
Size (as large as fits down to 25%), Position (nine squares), Paper
(automatic, upright, sideways) and Fill the whole page, which says it
cuts the edges off and is never the default. The preview is the PDF that
prints, laid out again after each change. Worksheet PDFs and lesson pages
get no picture controls and are unchanged.

Page backgrounds are no longer printed for lesson pages either, as in
Chrome; Blink darkens light text when it does that, so a white-on-black
page prints readable grey on white. "Colour" is "Color" in the print
interface and the manual.

Lab: 135 print checks, now including eight picture layouts measured on
the job file CUPS kept — byte-identical to the preview, within 1 point of
where asked, no ink anywhere else. Printed from the real session with the
pointer: 50% top left, Elmer-66, the same. Not yet on paper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 21:41:15 -05:00
ekklesiaandClaude Opus 5.5 6946bae455 release: 0.2.6.1, for the canary machine only
Wonder Ink prints — lessons and worksheets, through a preview — without
the 0.2.2 crash; lessons are the size the browser shows them and zoom;
the tab strip looks like the classroom; and four-part versions work end
to end, with a published version never used again.

Published to canary: ekklesia-classroom_0.2.6.1_amd64.deb, sha256
7bd3aabfbbd6f727be87fea366ecaefb40bd5ada37124c96703e8b9ccf510bc3.
Gated on that exact file as an upgrade from 0.2.6 (the laptop) and from
0.2.2 (the desktops). Stable still answers 0.2.2 and nothing here wrote
to it; the three desktops are unchanged until paper comes out of Elmer
and the owner approves — see docs/testing.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 19:59:58 -05:00
ekklesiaandClaude Opus 5.5 259c43bee8 docs: the 0.2.6.1 GUI checks, as the lab's Plasma session showed them
Printing from the real context menu and preview, Cancel, a fullscreen
page and its return, the restart after a hard kill, and a worksheet at
150% lesson size (the PDF viewer keeps its own zoom).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 19:59:58 -05:00
ekklesiaandClaude Opus 5.5 1d9da3bca0 test: rehearse the stable rooms' upgrade on every canary, and list Sunday's checks
update_canary.sh now also validates the exact package it is about to
publish as an upgrade from whatever stable names — 0.2.2 today, several
releases behind the laptop's canary. A package that upgrades the laptop
and breaks the desktops has to fail before it is published.

The lab print check runs in the Plasma lab as well as a plain container:
its rig lived in /tmp, which is a tmpfs there that docker cp cannot write
into, and its inline-PDF server shared a port with another test server.

docs/testing.md has the physical checks for 0.2.6.1 — paper through
Elmer, the real keyboard's zoom keys, the machine's graphics through
--diagnose, sign-out with a real account — before stable moves.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 19:38:04 -05:00
ekklesiaandClaude Opus 5.5 88ea3e86fd feat: measure why lessons load slowly, and give a technician the same tool
Measured before changing anything, and nothing about loading changed,
because the evidence does not point at Wonder Ink. Same lab, same page,
timed by each engine until the sign-in form exists: Wonder Ink 2.86-3.00 s
cold and 2.20-2.49 s warm, Chrome 2.79-3.48 s and 1.96-2.15 s. The site is
the weight — 119-160 requests and 3.2 MB for a sign-in form, and its
console reports loading the Google Maps API twice. Memory about 610 MB
for one tab, 115 MB per further tab; idle CPU about 1%.

The lab draws in software for both engines, so it cannot show the one
likely cause it has no hardware for: QtWebEngine failing to use a
classroom's GPU where Chrome succeeds. classroom-wonderink --diagnose,
run from the maintenance terminal in the teacher's session, prints the
engine versions, the scale, the WebGL renderer (a driver, or software),
and the sign-in page's time from a cold cache, in a throwaway profile
beside any running Wonder Ink.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 19:23:20 -05:00
ekklesiaandClaude Opus 5.5 1cb9fd35d2 feat: Wonder Ink looks like the classroom, and its preview shows the page
The tab strip is drawn from the classroom's own palette, the one the
panel, Parent Call and Presentations read: larger titles, an accent
under the active tab, worksheets marked with a document icon. Its one
corner widget is the lesson's size and Print — not a new-tab button. With
one tab the strip stays hidden, as before; a change of size shows a
moment's badge instead, and the right-click menu and keys still work.
The window is named "Lesson — Wonder Ink" and carries its own icon.

The print preview showed "Page 1 could not be shown" in the real
session while printing correctly: poppler 25.03 does not write to stdout
when asked for "-" — it writes a file called "-.png" into the current
directory, which for Wonder Ink is the teacher's home. Pages are now
rendered into a directory of their own, and the lab print check fails
any preview that does not draw its page.

The teacher's manual has lesson size and printing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 19:13:04 -05:00
ekklesiaandClaude Opus 5.5 48b3097c43 feat: a lesson in Wonder Ink is the size the browser shows it, and zooms
Teachers found lessons in Wonder Ink too small next to the ordinary
browser. Measured by asking each page for devicePixelRatio: Wonder Ink
follows the display scale exactly (dpr 1.5 at 150%), and Chrome is
scaled TWICE (dpr 2.25) — under Plasma it follows the compositor itself
and classroom-browser also forces UI_SCALE, a flag from the sway session.

The browser is left as it is. Wonder Ink's pages start at zoom =
UI_SCALE, the room's own number (1.0 at 100%), and measured afterwards
match the browser: dpr 2.25 and 853 px against 2.25 and 854. The window
is not zoomed; Qt scales it once, correctly.

Ctrl+Plus/=/keypad, Ctrl+Minus, Ctrl+0, Ctrl+wheel and the right-click
menu set one level for every tab, remembered as LEVEL= in its own file,
relative to the room. Two actions on one key are ambiguous to Qt and it
then does neither — Ctrl+Minus did nothing until each command became one
action with all its keys, which also fixes Ctrl+Shift+Tab.

The engine profile is built by one function, which the lab's checks now
use: a check with its own profile showed every worksheet blank.

The lab fences 10.10.0.0/16 as well: the development host reaches the
real Elmer over Tailscale, and the lab provisions a queue pointing at it.
Four lab checks written for older code are corrected; one had failed on
every run since 0.2.3.

tools/lab/wonderink-zoom-check: 18 checks. Lab: converged 120, print 39,
worksheets 24, all passing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 19:04:58 -05:00
ekklesiaandClaude Opus 5.5 10433b850d fix: Wonder Ink prints without crashing, and prints worksheets at all
Little Waders, 0.2.2: SIGSEGV in QPrinter::resolution() every time a
teacher confirmed a print job. QWebEngineView.print() keeps the raw
QPrinter pointer and returns; Qt's didPrintPage() reads
printer->resolution() once Chromium has rendered the page, and by then
PyQt had deleted the printer, which print_view() held only as a local.
Reproduced in the lab with the 0.2.2 code, same backtrace, no job.

Keeping the printer alive fixed the crash and was measured, and Qt's
path was still not a print path: a worksheet PDF tab printed as one
blank page, a lesson went as 24.6 MB of 300 dpi pictures with no text,
and a queue refusing jobs was reported as printed.

Print is now: make a PDF (a worksheet already is one; a lesson page is
printToPdf'd onto the printer's own paper; an inline PDF is downloaded
and opened as a worksheet), show it in a preview with printer, pages,
copies and colour, and hand it to CUPS with lp — Chrome's route in the
same rooms. Nothing asynchronous holds a printer. Black and white uses
the driver's ColorModel=Grayscale, because Elmer's generic PCL 6 driver
ignores print-color-mode.

An exception escaping any slot no longer aborts Wonder Ink: PyQt6's
default is to kill the process, and it is now logged instead.

tests/test-classroom-printing.sh: 50 checks. tools/lab/wonderink-print-
check: 39 checks over 13 scenarios in the lab, against a queue built
from Elmer's PPD, judged by what CUPS received. Not yet on paper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 18:36:29 -05:00
ekklesiaandClaude Opus 5.5 690ef95aff feat: four-part versions, and a published version is never used again
0.2.6.1 is a maintenance release of 0.2.6. The build refused four
parts, update_canary.sh's automatic bump could not add one to "6.1", and
neither publish script would have noticed a version being published
twice — which is what happened to 0.2.6 this morning: it was rebuilt
with the Presentations hardening and uploaded over the first 0.2.6, and
a room that had installed the first can never be offered the second.

One release library now holds the version pattern (three or four parts,
no leading zeros, no .0 maintenance), orders by dpkg --compare-versions,
refuses a version that has ever been published or is not newer than all
of them, uploads with --ignore-existing and checks the server's
checksum, writes a manifest only for a package the server holds, and
reads the channel back over HTTPS afterwards. update_canary.sh takes the
version explicitly; update_stable.sh refuses to go backwards.

The classroom updater refuses a manifest whose version, package name or
checksum is malformed, and refuses a download whose own control file is
not the version the manifest promised, so a room can never loop on
reinstalling and rebooting.

tests/test-release-versions.sh: 87 checks against a stand-in server and
real dpkg. The Trixie gate now also rehearses a stable room: 0.2.2 to an
interim 0.2.6.1 upgraded with settings, channel and Presentations=false
intact, and the installed updater answering correctly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 18:19:37 -05:00
ekklesia 6f2d169355 fix: harden classroom presentations 2026-09-23 00:02:25 -05:00
ekklesiaandClaude Opus 5 ec1e0d1cd7 release: 0.2.6, for the canary machine only
Presentations, the teacher's manual, and the Wi-Fi hand-over fix.

Canary only. The three classroom desktops are on CHANNEL=stable, which
still answers 0.2.2 and which nothing here has written to.

The prayer-room laptop is the one machine this is for, and its Wi-Fi is
already NetworkManager's — `classroom-wifi --adopt` reports "nothing to
adopt" on it and changes nothing, which is asserted by a test rather
than remembered.

Note for whoever installs it: this release adds LibreOffice and the font
set, so the upgrade fetches 216MB across 186 packages from Debian's
mirrors. It needs the network up at the time and it is not a five
minutes before the study job.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 21:03:55 -05:00
ekklesiaandClaude Opus 5 32ee0b544a fix: check the fonts by the files LibreOffice opens, not by fc-list
The validator asked fc-list whether Liberation Sans, Carlito and Caladea
were installed. fc-list belongs to the fontconfig package, which nothing
in this classroom pulls in, so the check could only ever fail — and it
did, twenty-odd minutes into a release run, for a reason that had
nothing to do with the fonts. They were all present the whole time.

Asserted by path now, which is what LibreOffice actually looks for.

Also depends on fonts-liberation rather than fonts-liberation2. The
latter is a transitional package in Trixie that exists only to depend on
the former, and a release should not name a package whose whole purpose
is to go away.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 20:47:57 -05:00
ekklesiaandClaude Opus 5 9c864a1e44 test: the release gate is bounded, which is not the same as a number
These pinned "720s" and "candidate-install 360" as literal strings. When
0.2.6 raised those budgets for LibreOffice, the install check failed —
and the outer-deadline check went on passing, because the comment
explaining the change still contained "720s".

A test a comment can satisfy is not testing anything. What matters is
that nothing in the gate runs unbounded and that each budget is long
enough to install a desktop, so that is what it now reads.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 20:37:06 -05:00
ekklesiaandClaude Opus 5 89b24acff0 fix: the release gate has time to install what the package now depends on
The validator installs the previous package and then the candidate, two
full desktop installs in one container, on a 720s cap with 360s per
install. 0.2.6 adds LibreOffice and a dozen font packages — 216MB and
186 packages on top of Plasma — and the run was killed during the
PREVIOUS install, before it reached a single assertion.

Raised generously rather than to just-enough. This is the gate that has
to say "the package is wrong"; one that says "the machine was busy"
instead teaches everybody to re-run it until it passes, which is how a
gate stops being one.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 20:33:51 -05:00
ekklesiaandClaude Opus 5 95ae054f6f fix: the presenter stops insisting on fullscreen after a few refusals
The slideshow re-asserts fullscreen when a compositor takes it away,
which is what keeps a teacher from being left looking at the library
beside their slides. A compositor that granted it and revoked it
repeatedly would have had this asking forever, in the middle of a Bible
study.

It gives up after five goes. The slides are still on the screen and
Escape still works; only the insisting stops, which is a far better
failure than a window fighting a compositor in front of a room.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 20:24:06 -05:00
ekklesiaandClaude Opus 5 f2942a2946 docs: one manual, the pages a device has and a printable copy of all of them
Documentation becomes part of the release rather than a job afterwards.
docs/manual is now the only place teacher instructions are written, and
tools/build-manual produces both versions from it:

  classroom-manual.html   installed on every machine, showing only the
                          features THAT device has
  classroom-manual.pdf    the universal printed manual, every feature,
                          with a conditional sentence above each
                          optional page so it stays true in a room that
                          does not have them all

They cannot drift apart because neither is written by hand, and
build-release.sh builds and checks them on every release: a page that
declares a feature with no printed-if fails the build, because that is
exactly how the printed manual comes to state an optional feature as
though every room had it. So does a page naming a feature device.conf
does not have, and an optional feature with no page at all.

WHAT REACHES THE CLASSROOM ACCOUNT. Only a list of setting NAMES, and
only ones the package already defines as optional applications.
classroom-desktop-layout — already the one place that decides what a
device offers — writes them to /run/ekklesia-classroom/features.json,
and classroom-manual filters that against FEATURES before using it. No
values from device.conf, no broker password, no addresses. With no
manifest at all every page is shown, which is the printed manual and is
never wrong, only less specific.

Help is a Quick Launch button beside Parent Call, deliberately not in
the row with the applications: putting it there would move buttons
teachers reach for without looking. Every room gets it, because a
teacher who needs the manual is by definition one who does not know
where to look.

The PDF is written directly rather than converted. A manual is
headings, paragraphs, bullets, steps and one table; writing that with
the standard fonts is a couple of hundred lines and no dependency, so a
release can be built on any machine with Python. Pictures are embedded
in the digital manual as data URIs so it stays one file that works
wherever it is copied and with the network unplugged; the printed
manual names them instead, which is the one thing this builder does not
do and is noted where somebody would look for it.

Two defects found building it, both of which hung or corrupted the
output rather than failing: a paragraph opening with a bold word
matched the bullet guard but not the bullet rule, so nothing consumed
it and the build spun; and a list item wrapped across two source lines
ended the list, so a three-step procedure printed as three steps all
numbered 1.

Also adds the 0.2.6 physical checklist to docs/testing.md and the
release validator's checks for the Wi-Fi regression, the conversion
pipeline on a real Debian, the fonts, the manual and the dock order.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 20:21:39 -05:00
ekklesiaandClaude Opus 5 55f4c183c7 feat: Presentations, for a teacher who downloaded tonight's session
PRESENTATIONS_ENABLED, off by default, independent of everything else. A
teacher downloads a session in ordinary Chrome and it is on the
Presentations screen a few seconds later, ready for the television. No
file manager, no import button, no Impress.

WHY NOTHING LAUNCHES A DOCUMENT APPLICATION. The brief was to use
existing presentation software, so the existing software was tried
first, on Debian 13, before any of this was designed. `soffice --show`
does start a real fullscreen slideshow — and needs XDG_SESSION_TYPE plus
a VCL plugin or it dies with "Failed to open display", shows a splash,
opened a "Tip of the Day: 1/225" dialog over the slides, and leaves the
Impress editor when the show ends. `okular --presentation` opens THREE
windows: the presentation, the ordinary document window behind it, and
an information dialog. zathura is not a presenter. pdfpc is the closest
and is another dependency with a presenter-console model. Every one of
them leaves an application on screen that can open any file on the
machine, and none of them knows there is a library to go back to.

So LibreOffice converts once, headless, at import, with no window on any
screen; poppler renders the pages; the classroom shows the pictures.
Measured: 0.34s for a ten-slide PPTX, 0.03s for its ten thumbnails,
0.10s for one slide at 1920px. That is not a new renderer — it is the
same engine Impress would have used live — and it means Escape is ours,
presenting is instant, and a PDF exported from Google Slides skips the
conversion entirely and keeps the appearance Google gave it.

THE THREE THINGS KEPT APART. The library thumbnail, the starting slide
and the retention clock are independent, because the first slide of a
Bible study session is a generic opening shared by every session and the
one that names it is usually the second. Quick Look chooses the picture;
it does not change where the slideshow starts and it does not count as
having presented anything. mark_presented() is called when a slide is
actually drawn, not when Present is pressed: an intention is not a
presentation, and a viewer that failed to start must not buy another
thirty days.

IMPORTING. The teacher's download is copied, never moved, renamed or
deleted. A file with a .crdownload beside it is still arriving. Wonder
Ink's own folder is not looked at. Two downloads of the same name are
two presentations, the second titled (2) — found in the lab, where a PDF
and a PPTX of one session both came out with the same name because the
import being named was excluded from the taken-titles set BY TITLE, and
a set holds one copy of a repeated string.

Metadata is written saying "importing" before any work, so a reboot
mid-conversion leaves a directory the next startup sweeps and imports
again. A file that cannot be converted gets a card saying so, offering
only Remove, with the teacher's download untouched.

Retention: 180 days never presented, 30 days from the last presentation,
every presentation starting the thirty over. The cleanup never touches a
presentation being shown, never leaves the library directory, and never
looks at Downloads.

The dock order is unchanged: Presentations is appended after the disc
player, so Wonder Ink, the browser and the player keep the places
teachers have been reaching for. There is a test for exactly that.

classroom-wonderink now asks feature() rather than carrying its own copy
of the default, so the panel and the command cannot disagree.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 20:21:19 -05:00
ekklesiaandClaude Opus 5 30d2d99230 fix: a hidden Wi-Fi survives the hand-over, and a failed one leaves an owner
The prayer-room laptop's 0.2.5 migration built a NetworkManager
connection, could not bring it up, rolled back, and left the wireless
adapter belonging to nobody. Four separate defects, each of which would
have been enough on its own.

THE HIDDEN FLAG WAS THROWN AWAY. The stanza said wpa-scan-ssid 1 and the
parser read it — into options, where the only accessor ignored it. The
profile came out with 802-11-wireless.hidden: no, and NetworkManager
will not associate with an SSID it has not seen in a scan. A hidden one
is never in a scan. credentials() returned a pair; profile() now returns
the whole network, and add() writes the flag in BOTH directions rather
than only when true, because a default that happens to be right is not
the same as an answer.

IFDOWN RAN AFTER THE STANZA WAS COMMENTED OUT. ifupdown reads
/etc/network/interfaces to find out how to bring an interface down; with
the stanza gone it says "Ignoring unknown interface" and does nothing.
So ifupdown's own wpa_supplicant and DHCP client were still holding the
radio when NetworkManager was asked to associate with it, which is a
fight NetworkManager loses. Down first, while the file still says how;
rewrite the file second.

THE ROLLBACK LEFT THE ADAPTER UNMANAGED. restore() ended with
`device set <dev> managed no`, which is right while ifupdown can bring
the interface up and is a radio nobody owns when ifup has failed too.
That is the state the laptop was found in, and why `nmcli device set
wlp2s0 managed yes` was a manual recovery step. It now escalates: if
ifupdown cannot get the network back either, the interface goes to
NetworkManager rather than to neither, and it says so loudly.

A '#' IN A PASSWORD WAS SILENTLY TRUNCATED. The parser cut every line at
its first '#'. interfaces(5) is explicit that only whole-line comments
exist, so a pre-shared key containing one became a prefix of itself —
and the only symptom is an association failing exactly the way a wrong
password fails.

Two smaller ones went with them. The rollback un-comments its own marked
lines in place rather than copying the backup over the whole file, so a
wired change made between the migration and the rollback is no longer
silently reverted. And nmcli terse output is split on its own escaping
rules under LC_ALL=C, so a network whose name contains a colon cannot
make a successful migration look like a failed one.

--adopt now also refuses a network it cannot recreate before taking the
interface away from ifupdown to find out, and reports an
unmanaged-devices line in NetworkManager's configuration, which survives
the stanza going away.

Verified against a real NetworkManager 1.52.1: the keyfile carries
hidden=true, the SSID keeps its space, and a psk containing '#' is
intact, at 0600 root:root with no connection.permissions. The new test
drives the whole hand-over against stubbed tools and asserts the
ordering, the rollback and the managed state — including that a machine
already migrated, which is the laptop, is not migrated again.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 20:20:53 -05:00
ekklesiaandClaude Opus 5 fe08363534 fix: a machine with no ifupdown configuration is not an error
The release validator caught it: in a container there is no
/etc/network/interfaces at all, and a missing file was being treated as
a read failure, so --check printed nothing and said nothing was wrong
rather than saying there was nothing to do.

A machine with no ifupdown configuration is the ordinary case for a
fresh install, and the answer is simply that this command has no work.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-21 21:23:30 -05:00
ekklesiaandClaude Opus 5 b8688d00a7 docs: the Wi-Fi hand-over is a physical check, so write down its steps
No lab has the laptop's radio, so the one thing that cannot be proven
here is the hand-over succeeding. These are the ten steps that prove it
on the machine, including the four that prove the classroom account
still cannot read the password afterwards.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-21 21:11:49 -05:00
ekklesiaandClaude Opus 5 35e3bdcbbd fix: give a Wi-Fi the installer set up to NetworkManager, by hand
The Bible study laptop had working internet and a panel that said there
was no connection. Both were true, and one thing caused both.

Measured on the machine:

    nmcli device status          wlp2s0  wifi  unmanaged
    nmcli networking connectivity  none        while curl fetched fine
    nmcli device wifi list       nothing, with the radio enabled
    /run/network/ifstate         wlp2s0=wlp2s0
    a second wpa_supplicant, -i wlp2s0, with no -c config file
    dhcpcd holding the lease
    /etc/network/interfaces      0600 root:root

Debian's installer had been given the Wi-Fi, so it wrote it into
/etc/network/interfaces and ifupdown brings it up. NetworkManager ships
with plugins=ifupdown and [ifupdown] managed=false, so an interface named
in that file is UNMANAGED — which is why the applet had nothing to report
and why scanning found nothing. An unmanaged device is not a radio
NetworkManager will scan with. rfkill was ruled out separately: the radio
is enabled.

managed=false stays; it is what keeps a tower's static Ethernet where an
administrator put it. Only a wireless interface is moved.

classroom-wifi grows --check, --adopt (with --dry-run) and --rollback.
--adopt builds a NetworkManager SYSTEM connection from the SSID and key
already in that root-only file, so the password never leaves the machine
and never reaches a human or a log; it lands in a 0600 root file with
psk-flags=0, the same shape 0.2.4 established, and the polkit rules
protecting it are untouched.

Deliberately NOT run by the upgrade. It moves a machine's only link
between managers and the machine it could strand is a laptop in a locked
room, so classroom-migrate only looks and prints what it found. An
administrator runs it over SSH while watching.

Two things this learned from testing the rollback rather than from
reading the code, and both are why the rollback is worth testing:

  * ifdown was missing in the lab, subprocess raised FileNotFoundError,
    and the exception escaped AFTER the stanza was commented out. No
    rollback ran. Every hand-over command now goes through a runner that
    tolerates a missing binary, and the whole hand-over is inside a guard
    that restores the file on any failure at all.
  * it now refuses to start when ifdown or ifup are absent, because
    without them the hand-over would be a one-way door.

Success also requires the interface itself to be carrying the connection,
not merely that the machine is online — a machine with a second link
would otherwise report a failed migration as a success.

Verified in the lab against real NetworkManager with a real ifupdown
file: --check and --dry-run leave it byte-identical, a failed --adopt
rolls back byte-identically, and --rollback restores a half-migrated
machine byte-identically and exits 0. The one thing no lab can do is the
hand-over succeeding, which needs the laptop's radio.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-21 21:11:19 -05:00
ekklesiaandClaude Opus 5 a31fa75d55 fix: the bar keeps its order when an application opens
On the laptop the browser jumped behind the disc player the moment it was
opened. 0.2.4 caused that, and the cause is two lines of the stock applet:

    main.qml:153  hideActivatedLaunchers: tasks.iconsOnly || ...
    main.qml:155  launchInPlace: tasks.iconsOnly && sortingStrategy === 1

For an icons-only bar the first is always true — a launcher is hidden the
instant its application starts — and the second is what puts the running
window back where the launcher was. It only does that at sortingStrategy
1. 0.2.4 set it to 0 to stop teachers dragging the icons about, so the
launcher vanished and the window was appended after the others.

Reproduced in the lab with a real Chrome window before changing anything,
and the bar read off the screen: Wonder Ink, the player, then the
browser. With 1 it is Wonder Ink, the browser, the player, through
opening, a second window, another application opening beside it, and
every close.

The launcher identity was suspected and cleared: only one browser button
appears, because google-chrome.desktop already carries Chrome's own file
name and StartupWMClass for exactly this reason.

THE COST, AND IT IS REAL. Manual sorting is also the only mode in which
MouseHandler.qml accepts a reordering drop, so a teacher who knows to drag
can rearrange the bar again. There is no value that gives both and no
Kiosk key that separates them. classroom-desktop-reset already rebuilds
the bar from the package at every login, so a drag never outlives the
session. docs/testing.md states the trade rather than burying it, and the
configuration escape is written down there too as still open.

tools/lab/order-check is the test this needed and did not have. 0.2.4
passed a full set of configuration assertions while the bar did something
else, so this one opens real application windows and reads the panel
strip as pixels — there is no image library in the lab, so it captures
PPM and names each icon by colour. Six checks, all of them behaviour.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-21 19:34:15 -05:00
ekklesiaandClaude Opus 5 0fa26f398d release: 0.2.4, for the canary machine only
The housekeeping release: independent feature settings, the Bible study
laptop's display mode, the Wi-Fi credential rules, the panel order and the
permanent download addresses.

0.2.3 is already published and installed on the canary, so it is left
exactly as it is. Stable stays at 0.2.2 and the stable manifest is not
touched by this.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 21:27:05 -05:00
ekklesiaandClaude Opus 5 a5c7d1d057 fix: an application a device does not offer does not start either
Taking the launcher off the bar is half of a feature switch. The command
has to refuse as well, or a device that does not offer an application can
still be talked into running it by a stale desktop entry, a leftover
window rule or a technician's muscle memory.

Wonder Ink already did this. Parent Call now does the same, through the
same helper and with the same default: absent means yes, so every
existing room is unaffected.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 21:19:41 -05:00
ekklesiaandClaude Opus 5 52301084a5 feat: an external-only display mode, chosen rather than inferred
The women's Bible study laptop is a fixed appliance: AC power, battery
out, lid shut, one television on HDMI. Its own panel should be off.

DISPLAY_MODE=external-only says so, and it is a setting rather than
something inferred from having a lid, because another laptop may want its
own screen or both. Nothing reads DEVICE_TYPE to decide it.

The built-in panel is identified by kscreen's output TYPE — 7 is Panel —
rather than by connector name. Matching "eDP-1" would be matching one
machine's spelling: eDP, LVDS and DSI are all internal, numbered
differently per driver and renamed between kernel versions. The names are
kept only as a fallback for a driver that reports no type, which some
virtual devices do.

One rule overrides the setting: never leave the machine with no screen. A
laptop asked for external-only whose television is off keeps its own
panel and says so in the journal. A machine nobody can see is one nobody
can fix standing in front of it, and it corrects itself the next time
this runs with the set on. Only the outputs that are not already right
are touched, so a television power cycle is not a stream of mode sets.

Applied at login from an autostart entry named to sort before the scale
one, so the scale lands on the displays this leaves enabled.

The lid itself needed no new code — logind ignores every lid event,
PowerDevil's lidAction is 0 in all three profiles with
triggerLidActionWhenExternalMonitorPresent=false, the profiles are
immutable and the classroom account cannot reach suspend through polkit.
That was all already true for towers. What was missing was a test saying
so, and there is one now, because it is the property the laptop depends
on and nothing would have noticed it regressing.

KNOWN LIMITATION, and it is written into docs/testing.md rather than left
to be discovered: the mode is applied at login, not continuously. If KWin
re-enables the internal panel when the television is power cycled, it
stays enabled until the next login. The lab has no lid and no HDMI
hotplug, so this cannot be settled here; the physical procedure says to
check it specifically, because the answer decides whether the follow-up
needs an event-driven applier like the audio policy's.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 21:17:06 -05:00
ekklesiaandClaude Opus 5 886f8a70d2 feat: one download address per channel that never changes
https://updates.riverofglorymn.org/stable/latest.deb
  https://updates.riverofglorymn.org/canary/latest.deb

A symlink beside each channel's manifest, pointing at the versioned
package the manifest names. Deliberately not a second record of which
version is current — the manifest stays the only answer to that, and the
publish scripts derive the pointer from it rather than from anything they
know themselves.

The versioned files are never rewritten. A release uploads its package,
then the manifest, then swaps the pointer by rename. Atomic, so nothing
downloads a half-written pointer; and last, so the pointer can lag the
manifest by a moment and can never lead it. A pointer that led the
manifest would offer a package the channel had not published.

Both latest.deb and manifest.json are served no-store on the update
server. Cloudflare sits in front of it and caches .deb by extension, so
without that header a room could be handed the previous release from an
edge for hours. The versioned packages keep no such header because they
are immutable and worth caching.

The server change is one matcher in the Caddyfile's updates block, and it
took three attempts to land for a reason worth recording: the Caddyfile is
a FILE bind mount, so `mv` over it replaces the inode and the container
goes on serving the old one. Two apparently successful reloads changed
nothing. It is written through the mount now, and the host copy is
identical so the next container restart is a no-op. Caddy was reloaded,
never restarted; the church's other sites were not interrupted.

Installing from a channel URL still does not move a machine onto that
channel. CHANNEL in device.conf decides which manifest the updater
follows, and the postinst only appends settings that are missing, so
fetching the canary package by hand to repair a machine leaves it on
stable.

Verified end to end against the live server: both URLs deliver bytes whose
sha256 matches their channel's manifest, both carry the no-store header,
and the versioned package does not.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 21:11:35 -05:00
ekklesiaandClaude Opus 5 bf7b959893 feat: the room may use the church Wi-Fi and may not read its password
These are shared computers in rooms full of people, and in a stock Plasma
desktop "use the network" and "read the network's password" are much
closer together than they look.

Measured on Debian 13 with NetworkManager 1.52 and Plasma 6.3:

  * org.freedesktop.NetworkManager.settings.modify.own ships as "yes" for
    an active session — no prompt, no administrator. It covers any
    connection whose connection.permissions names that user, and a Wi-Fi
    network added from the desktop is created exactly that way. So a
    password a teacher types in is readable by the next person to sit
    down.
  * the applet offers "Show Network's QR Code" on every saved wireless
    connection, and the QR code contains the pre-shared key.
  * that action, and the "Configure…" beside it, are contextual actions in
    ConnectionItem.qml and do NOT consult
    authorizeControlModule("kcm_networkmanagement"). The Kiosk key gates
    the toolbar button only. Hiding buttons was never going to be the
    answer.

Every one of those paths ends at NetworkManager's GetSecrets, so that is
where this is answered. A polkit rule refuses settings.modify.own,
settings.modify.system, both wifi.share actions and the machine-wide
settings, for the classroom user by name.

Deliberately NOT refused: network-control, wifi.scan and the
enable/disable actions. None returns a secret, and they are what
connecting and seeing a network are made of. Automatic reconnection does
not go through polkit at all — NetworkManager does it as root — so nothing
here can stop the room coming back on the network after a reboot, which is
the property that matters most on a Sunday.

An administrator's account and root are untouched, and the override route
is the same one the sleep rule has.

classroom-wifi is the other half, and it is a requirement rather than a
convenience: it provisions the church Wi-Fi as a SYSTEM connection, with
no connection.permissions, psk-flags=0 and a 0600 root:root file. A
connection the machine owns is one nobody in the room can read. Verified
against real NetworkManager in the lab — the classroom account gets
"Permission denied" on the file, "--" from nmcli and "Insufficient
privileges" from GetSecrets, while autoconnect stays on.

There is deliberately no teacher-facing join dialog. Anything that lets
the room add a connection lets the room own it, and an owned connection is
a readable one; docs/networking.md says so rather than leaving it to be
rediscovered.

The rule is plain JavaScript, so the test evaluates it with polkit's
objects stubbed and asserts both halves: which actions come back NO, and
which are left alone. What it cannot test is the last mile — polkit
resolves a subject through logind and a container has no graphical seat
session, so every check there is denied for reasons unrelated to this
rule. That one is a physical check, and it is written down as one.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 21:04:07 -05:00
ekklesiaandClaude Opus 5 7f4a122900 feat: one setting per application, and never the shape of the machine
A laptop is going in for the women's Bible study, and the obvious way to
build that — a laptop profile that turns off the children's church
applications — is the wrong way. A laptop may well take children's church,
and a tower may well be a Bible study. Hardware cannot be allowed to
decide purpose, so every optional application gets a setting of its own:

    WONDERINK_ENABLED       already existed; behaviour unchanged
    DVD_ENABLED             new
    PARENT_CALL_ENABLED     new

and DEVICE_TYPE says only what the hardware IS. classroom-desktop-layout
never reads it, and there is a test that says so, because this is the rule
that will be under pressure the first time somebody adds a device.

All three default to true, so an existing classroom that has never heard
of any of them comes up exactly as it did before. The settings are
appended to device.conf by the same mechanism every other setting has
used, so an upgrade gains them without losing a byte an administrator
wrote.

The disc player is the one place hardware still votes, and it is a veto:
DVD_ENABLED can take the player away from a machine that has a drive,
nothing can put it on a machine that has none. A button that cannot work
is worse than no button.

The layout's launcher markers are replaced by a feature block of three
plain booleans. The old mechanism commented a line out of an array, which
worked for launchers and could not express Parent Call, because that is a
whole widget rather than one entry. Three booleans express both, and the
launcher list is now built from them in one fixed order, so a disabled
application closes the row up rather than leaving a gap, and the same
enabled set produces the same bar on every machine in the church.

Also here, because it is the bar and not a feature: sortingStrategy=0.
Plasma's default is 1, "manually", and the task manager accepts a
reordering drop only in that mode — MouseHandler.qml tests
tasksModel.sortMode === SortManual before it will move anything. The
classroom never wrote the key, so it had the default, and any teacher
could drag the launchers into a different order. This is the applet's own
lever refusing the drop rather than something of ours putting the bar back
afterwards.

The test now runs the layout the package actually ships, in node, with
Plasma's API stubbed, and asserts the launcher string that comes out.
Asserting against a copy of the file's text in the test was how the old
one worked, and a copy is a thing that drifts.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 20:55:09 -05:00
ekklesiaandClaude Opus 5 fc764b0621 release: 0.2.3, for the canary machine only
The audio reliability fix, built as its own version. 0.2.2 is already
published and installed on the rooms, so it is left exactly as it is.

Stable stays at 0.2.2 and the stable manifest is not touched by this.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 14:48:44 -05:00
ekklesiaandClaude Opus 5 27cd64a668 fix: the room asks for its output again, not only once at boot
River Kids came up silent on stable 0.2.2. The decision this policy makes
was right and was never asked for.

classroom-audio applied its policy once at startup and then only when a
PipeWire event arrived. pactl talks to PipeWire's PulseAudio server, and
the unit was ordered after pipewire and wireplumber but not after that.
At boot it therefore raced the server and lost: the single startup apply
failed with "Connection refused", the subscription attached cleanly a few
seconds later, and from that moment the daemon waited for an event that
had already happened. systemd said active, the subscription was live, and
the policy had never run at all.

That is why the card was found on output:analog-stereo+input:analog-stereo
with its HDMI port available. That profile is WirePlumber's own pick, not
one this policy would ever produce, which is the evidence it never ran.

Reproduced in the lab against real PipeWire 1.4.2 and WirePlumber 0.5.8 by
starting the daemon with pipewire-pulse stopped and then starting it: the
old daemon sits idle forever, the new one converges.

The shape is now: wait for the server, apply, subscribe, apply again the
moment the subscription attaches, and coalesce each burst of events into
one decision. Applying on attach is the fix for the race. While the room
has not got the output it would prefer it also looks again on a backing
off timer capped at a minute, so a television switched on hours later is
picked up whether or not ALSA's jack change ever reaches us as an event.
Once the room has what it wants it stops looking entirely, and a machine
with no digital output never looks at all.

Also here, all of it found by measuring the real server rather than
reading the code:

  * sink lookup matched on fields a sink does not have. Two of its three
    conditions could never be true; the one that worked returned whichever
    sink of the card came first rather than the one for the chosen port.
  * volume and mute are no longer touched. The old code unmuted and forced
    70% on every output change, so every television power cycle reset a
    volume the teacher had set and could unmute an output muted on
    purpose. WirePlumber already remembers volume per device. A machine
    this policy has never configured still gets 70% once.
  * the profile chosen keeps the card's input, which is the difference
    between output:hdmi-stereo and the output:hdmi-stereo+input:analog-stereo
    an administrator reaches for by hand.
  * a card profile is only set when it is not already in place, and only
    so many times, so a sink that never appears cannot become a switching
    loop.
  * a machine with no sound server at all says so once and backs off,
    rather than filling the journal. pactl subscribe's own stderr is
    dropped because saying it is this daemon's job, and a subscription is
    only treated as healthy once it has lasted, since pactl starts
    successfully and then fails to connect.
  * the fixtures described a machine nobody has. Real pactl puts no
    "direction" on a port at all and says "availability unknown" rather
    than nothing, so a test could not have caught this.

Existing streams are moved explicitly. Measured both ways on the real
stack: changing the default sink does move a stream that never asked for
a particular sink, but a stream pinned to a device stays where it is with
no policy running, and this moves it back without the application being
restarted.

tests/test-classroom-audio-daemon.sh drives the real daemon through the
boot race, a television switched on with and without an event announcing
it, a power cycle, a set that never comes on, and a room already correct.
It uses a pactl mock because no loopback or dummy sound driver has an HDMI
port, so the one thing a lab cannot prove is that the real jack change
arrives; docs/testing.md carries the physical procedure for that and says
not to take a passing build as evidence of it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 14:46:46 -05:00
ekklesiaandClaude Opus 5 20007238be release: 0.2.2, for the canary machine only
0.2.1 is already published to canary and a rebuild of it would be
different bytes under a version somebody has already installed, so the
worksheet fix goes out as its own version.

Stable stays at 0.2.0 and the stable manifest is not touched by this.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 22:40:31 -05:00
ekklesiaandClaude Opus 5 5f2a9098c1 fix: a worksheet a lesson downloads comes back as a tab
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]>
2026-09-19 22:40:16 -05:00
ekklesiaandClaude Opus 5 55a5aa3c99 release: 0.2.1, for the canary machine only
The version the two 0.2.1 features are built as. Stable stays at 0.2.0
and the stable manifest is not touched by this.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 21:29:17 -05:00
ekklesiaandClaude Opus 5 de5025ad22 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]>
2026-09-19 21:29:17 -05:00
ekklesiaandClaude Opus 5 23dff0b7b4 feat: close the chord that let a teacher hibernate the room
Ctrl+Alt+Delete opened Plasma's session screen and a teacher hibernated a
classroom computer from it. A hibernated room cannot be recovered by
anybody standing in front of it, which on a Sunday morning is everybody.

logout=false was already set and did not stop it. That restriction
governs Plasma's own actions — what plasmashell draws in a menu or an
applet. The chord is a KGlobalAccel shortcut registered by ksmserver and
it asks D-Bus for the greeter without consulting a Plasma action at all.
A restriction on a menu cannot see a key.

Three layers, because each one alone has a way past it.

The key. kglobalshortcutsrc unbinds every ksmserver session action and
PowerDevil's Sleep, Hibernate, PowerDown and PowerOff, each entry marked
immutable. It is deliberately not in the xdg directory with the other
settings: kglobalacceld opens that file as a SimpleConfig, so it reads
one file in the teacher's home and no system directory at all. Measured —
with it in the cascade, kreadconfig6 reported the chord unbound while the
shortcut server went on answering Ctrl+Alt+Del. classroom-desktop-reset
copies the package's copy into place before the shortcut server starts,
the same way the panel is copied out of the package at every login.

The policy. A polkit rule refuses login1.suspend* and .hibernate* for the
classroom user and for nobody else. This is the layer that matters:
a key can be unbound and anything reaching logind can still ask.
Refusing here rather than masking sleep.target keeps a technician's own
account working, and is also what tells Plasma to stop drawing the
button — CanSuspend and CanHibernate come back no. Hybrid sleep and
suspend-then-hibernate have no action of their own in systemd 257; both
are authorised through hibernate, so denying it denies all three.

The greeter. Its D-Bus activation is shadowed with /bin/false, the same
mechanism already used for the old notification daemon. Layer one removes
the caller, this removes the callee.

Measured in the lab, from the package, after a reboot: the escape
reproduced first with the shadow removed — the greeter really does start
— and then refused, exited 1, with it back. The chord reports nothing
bound. systemctl suspend as the classroom user is denied. The maintenance
chord and the display chord both still answer, by key code, after the
shortcut file is replaced wholesale. A root process may still suspend,
hibernate, reboot and power off. 182 checks pass across a logout and back.

Screen locking stays disabled, the power button still does nothing, and
reboot and shut down are not newly denied to anybody.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 20:50:36 -05:00
ekklesiaandClaude Opus 5 8db5e8dd9f release: the Plasma classroom becomes stable 0.2.0
0.1.50 was taken to the church and the things a container cannot answer
were answered on the hardware: boot, autologin, the panel after a reboot,
panel and browser scaling, Wonder Ink, Parent Call opening in the corner
and reaching the sanctuary with a typed name, readable and silent
notifications, HDMI audio and the volume keys.

The promotion is a fast-forward. The experiment was cut from the
published stable source and never diverged from it, so master reaches
this work without a merge and there is one history.

One thing changed for the release. The channel rules turn around:
classroom-v2 and hyprland are the obsolete channels now and a room on
either comes home to stable, while canary — where the next development
release goes — and any channel somebody named by hand stay where they
were put. A fresh installation has always defaulted to stable.

Nothing else moved. This is the 0.1.50 that was verified, with a version
number and a channel rule.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-13 09:15:00 -05:00
ekklesiaandClaude Opus 5 ba8768cee5 fix: a room that was set up, a card where its button is, and larger cards
Parent Call told rooms they had never been set up. Nothing about the
rooms was wrong. Read from the church broker: …/coordinator and
…/pending are retained and …/mode is absent, and the coordinator's own
source says why — it publishes the retained mode only in answer to an
operator writing to …/mode/set, and nobody ever has. Every classroom
waited a second and a half for a message that will never exist.

A missing mode is now read as the coordinator's own default rather than
as an unconfigured room: the room offers the call and lets the sanctuary
end answer, because that is the end that knows. A mode that really is
off still refuses, in the coordinator's words, and a broker that is
missing or unreachable still says which of those it is. Proved against a
rig broker made to match the church's exactly, with a call landing on
the sanctuary mock.

The card is anchored to the bottom-left corner instead of centred, by
layer shell, which is what the disc player's cover has used under KWin
for months. Anchoring rather than positioning is what makes it right at
1080p and at 4K without a coordinate anywhere. It falls back to the
centred card if the library is missing.

Notifications are sized for a room. Every Plasma measurement is a grid
unit and a grid unit is the height of the general font, so the font is
the lever — 15 point instead of Breeze's 10, which takes the card from
about 350x110 to about 410x145 at 100% and doubles again at 200%. Text,
icon, padding and buttons move together. The chime stays off. No shell
package: that path cost the panel and is not coming back.

And the product name is gone from everything a teacher sees. The
notification card is from Classroom, the disc player's from DVD.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-13 08:34:37 -05:00
ekklesiaandClaude Opus 5 bd55c5abd9 fix: let the migration hand device.conf to the one place defaults live
classroom-migrate knew how to remove a setting, move a channel and point
a room at the broker, and knew nothing about which settings the classroom
has gained since a room was installed. That list is in
classroom_config.py and it stays there: the migration now hands the file
to ensure_device_config when its own work is done, rather than growing a
second copy of it.

It matters for the command rather than the package. postinst already
called the shared helper before running the migration, so an install
converged; a technician running `sudo classroom-migrate` by hand on an
old machine got a file that was still short of LIVE_ENABLED,
AUDIO_OUTPUT and UI_SCALE. Both paths end in the same place now, and the
release gate runs the command by hand on an already-converged machine to
prove it.

The validator's own expectation was the release-blocking failure, and it
was not what it looked like: it stops at CHANNEL=canary, before it ever
reaches the LIVE_ENABLED count. canary is the sway line, and moving a
migrated room off it is deliberate — a Plasma room left there is handed
the old desktop back by the next release published to it. The fixture now
expects the move, checks that every setting this release has arrived
exactly once, and checks it again after a reinstall.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-13 07:46:08 -05:00
ekklesiaandClaude Opus 5 34fd02e08f feat: give every room the broker, one scale, and its own panel back
Little Waders took the Plasma build and reported what a container
cannot. A disc player on a machine with no optical drive, a clock nobody
at the back of the room could read, a browser at half the size of the
panel around it, and Parent Call with no broker to call.

The broker is the same for every classroom, so it is a default now and
not something to fill in room by room. classroom-migrate brings an
existing room to the same place without touching its name, its username
or its password, and Parent Call distinguishes a broker it cannot reach
from one that was never set up: those send somebody to look at
completely different things.

The disc player's detection was right and ran too late — alongside the
session rather than before it, and the panel is built from the layout the
moment a session starts. It runs before the display manager now, and
again from udev when a drive is plugged in.

UI_SCALE had a ceiling of 1.5, which cannot express a 4K television; a
clock pinned at 15 points, which is the same size at every scale; and a
browser that was never told the number at all. All three are fixed and
the room doubles together at 200%.

This also brings 0.1.47's work into the repository, where it had only
ever existed in a build directory under /tmp: the hidden display dialog,
its authenticated writer, the polkit action, and the church's own
wallpaper. One piece of it is deliberately left out. The custom volume
display needed a shell package of our own, and with one selected
plasmashell builds no panel at all — measured twice, with that build's
own files, on a classroom that deletes its panel configuration at every
login by design.

The lab connected to the real church broker the moment the package
defaulted to it. Nothing was left behind, which was checked, but the
guard that should have stopped it had never worked: it used iptables,
which the lab image does not have, and probed with /dev/tcp under dash,
where that always fails. Both reported success.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-12 19:05:09 -05:00
ekklesiaandClaude Opus 5 926668dc9b fix: make an existing classroom converge into the Plasma appliance
A real room upgraded to 0.1.45 and came up as stock Debian Plasma with
the old Waybar across the top, mako still catching notifications, and a
lock screen on an account that has no password.

The cause was one line, and it was in the lab: the harness handed the
session under test XDG_CONFIG_DIRS by hand, so every result it had ever
produced was a result about a machine somebody had already configured.
On a machine SDDM starts the session through PAM, PAM does not read
/etc/environment.d, and Debian's own plasma-workspace env.sh then set the
variable from scratch — so the classroom's settings directory was not in
the cascade at all. No layout, no lock policy, no Kiosk restrictions.

The settings now arrive the way Plasma delivers them, through a file in
/etc/xdg/plasma-workspace/env. Around that: the look and feel is
immutable on its own key so a stale profile cannot answer for it; the
palette is composed into the package, because naming a colour scheme does
not produce one and the dark bar had been coming from a technician's
hand; the desktop reset converges another desktop's appearance state and
keeps the room's own; and classroom-migrate stops, deactivates and masks
the old shell, including the D-Bus activation that brought mako back
while the panel was down — found only by killing the panel on a migrated
machine.

device.conf loses COMPOSITOR and comes off the hyprland channel, which
would otherwise have handed the room the experiment back.

The lab stops flattering the package: it restarts the machine, lets SDDM
actually autologin, and asks a fresh install and a machine upgraded from
the Hyprland package the same sixty questions. Both answer identically,
twice, and the panel still comes back from eight SIGKILLs.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 23:06:56 -05:00
ekklesiaandClaude Opus 5 6050144e5d docs: the power button, the maintenance chord, and why long-Escape should change
Records what was measured this pass and, as importantly, what was not.

KGlobalAccel will accept a bare Escape and its component carries both a
pressed and a released signal, so hold-and-cancel is expressible. What
could not be measured here is whether a global shortcut takes the key
away from applications, because key injection cannot reach the nested
session. The mechanism says it does — KWin matches shortcuts in an input
filter ahead of the focused surface — which would cost the classroom
every ordinary use of Escape. That is inference, and it is written down
as inference, with the thirty-second bench test that settles it.

The recommendation is to change the gesture rather than grab the key an
appliance uses most: hold-to-confirm on an on-screen control, which is
ordinary Qt inside our own surface, or a modifier chord like the one the
maintenance terminal just proved. Either keeps the deliberate ceremony
without an input daemon.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 15:24:28 -05:00
ekklesiaandClaude Opus 5 30a924d945 feat: the power button does nothing, and one chord opens a terminal
Two layers for the button, because one is not enough: while a session is
running PowerDevil holds logind's handle-power-key inhibitor and acts on
the button itself, so a logind drop-in alone would be overruled. logind
covers the machine before the session starts, after it ends, and if
PowerDevil is not running. Verified: after the drop-in is installed and
logind reloaded, it reports ignore for the power, suspend, hibernate and
lid keys, and the running session was not disturbed by the reload.

Holding the button still cuts the power. That is firmware, below any
operating system, and it is deliberately left alone as the only recovery
a person standing at a wedged machine has.

The maintenance terminal is Meta+Ctrl+Alt+Shift+K, declared in the
package rather than seeded into a teacher's configuration, where an
update could not reach it. KGlobalAccel read it and registered the
component; invoking it opened a working shell.

It needed a wrapper. The classroom's Kiosk policy sets shell_access=false
and Konsole honours it — measured: with the classroom configuration in
its cascade Konsole starts and says "Program to run not set." The
restriction is worth keeping everywhere else, so it is lifted for this
one process by leaving the classroom's configuration directory out of
its cascade. It grants no privilege: the classroom user has no sudo
rule, and that restriction was always a barrier in the interface rather
than a privilege boundary.

Konsole's own entry is shadowed and hidden from /usr/local/share, so
nothing is patched and Konsole's package stays maintainable. Nothing is
pinned to the panel; a task button appears only while a terminal is
open, and closing it returns the bar to three launchers.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 15:17:55 -05:00
ekklesiaandClaude Opus 5 699858a71e docs: say how to pick the lab back up
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 13:10:25 -05:00
ekklesiaandClaude Opus 5 ed702584d8 docs: refresh the lockdown screenshots against the packaged panel
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 13:10:09 -05:00
ekklesiaandClaude Opus 5 13aac85b87 docs: record Parent Call, the DVD migration and what is left to test
Three things in this window that a container cannot answer, and they are
now the top of the next-steps list: a real key pressed into Parent Call,
a television switched on after the computer, and a disc that plays.

Also brings the comments in the shared libraries up to date. The only
mention of sway left in the tree is the upgrade step that removes the
sway-era login shell from a room being upgraded.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 13:09:42 -05:00
ekklesiaandClaude Opus 5 e8078e37c1 test: one command for every recovery property the room depends on
Run against the lab as the package built it, not a session edited by
hand. The panel came back from eight SIGKILLs a second apart in about
2.2s each time, the compositor in 0.1s, the audio policy from three, and
neither Parent Call nor the browser dying disturbed the panel.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 13:05:18 -05:00
ekklesiaandClaude Opus 5 d2d30d6b0c feat: take the last of sway out of the classroom
The DVD player was the only thing still speaking a compositor the room
no longer runs. It asked sway's tree whether VLC's window had really
gone fullscreen, because VLC reports playing while its window is still
the black startup one the loading cover exists to hide.

VLC's own status was measured as a replacement and is not one: it
reports fullscreen true in the same 250ms poll as playing, because the
flag is the option it was started with and not a report about its
window. So the cover is held for a floor and capped by the deadline that
was already there, and the compositor is not asked anything.

Focus-or-launch went the same way. The session already takes an
exclusive lock, so a second one cannot start, and bringing an existing
one to the front is the panel's job.

classroom-window, classroom_sway and classroom_chrome are gone with it —
the last of them was reachable from nothing at all.

DVD also moves out of the status row, where it sat beside the clock as
though a disc were a machine reading. It is an application: it is in the
row with Wonder Ink and the browser, and classroom-provision removes it
from the layout on a machine with no optical drive, which retires
DVD_ENABLED. A computer that moves between rooms takes its drive with
it; nobody remembers to edit a file when it does.

The panel is rebuilt from the package at every login now. Plasma keeps
whatever panel it first built, so an update could change the bar in the
package and never change it in a room — measured, with an icon that
outlived the file it came from. Editing the live panel configuration was
tried instead and destroyed the panel, because the shell owns that file.
Removing it before the shell starts is safe and makes the desktop
stateless.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 13:00:50 -05:00
ekklesiaandClaude Opus 5 18cf0249dd feat: give Parent Call a mark that says what it does
Three marks were put on a real panel and judged at the size the bar
actually draws them. A bell sat inches from Plasma's own notification
bell and meant something entirely different. A handset with an outgoing
arrow read as a telephone feature. Breeze has people and it has
telephones and the two never meet, so this is drawn in Breeze's language
instead: an adult, a child, and one small speech bubble, on Breeze's
22-unit grid with its ColorScheme-Text class, so it recolours with the
session and needs no second file for the light theme.

Quick Launch carries it. The Icon widget copies the entry into the user's
home so an update could never change it; a task manager reads the entry
live but also shows every running window, which filled the auxiliary
corner with the lesson's tasks.

One defect found on the way, and it affected the Wonder Ink icon that is
already shipping: gdk-pixbuf recognises an SVG by sniffing the first
bytes, and a licence-length comment above the opening tag pushes it out
of that window. Every GTK program reported "couldn't recognize the image
file format" while Qt rendered the same file happily. The comments live
inside <svg> now.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 12:47:58 -05:00
ekklesiaandClaude Opus 5 323989ac61 fix: let an update change the Parent Call button
Two findings, both from watching the bar refuse to change.

The Icon widget copies the desktop entry into the user's home and uses
the copy. A classroom that had already run kept the old icon and the old
command for ever: the package no longer contained a bell and the bar kept
showing one. The button is a task manager holding a single launcher now,
which resolves /usr/share/applications live — the place an OTA update
writes to — and costs nothing, because the classroom already runs one for
the applications.

NoDisplay=true then hid the entry from the very index that resolves
launchers, and the bar drew a blank page. There is no application menu in
this classroom for the entry to clutter, so it is indexable now.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 12:38:27 -05:00
ekklesiaandClaude Opus 5 7790003c03 feat: let a teacher ask the sanctuary to call a child's parent
The coordinator already existed on the control computer and its contract
is frozen, so this is the classroom half of a contract that was read from
its README and source rather than guessed: four fields out, the status
topic subscribed before anything is published, and every word the teacher
reads taken from the coordinator's own message field.

It is a separate process, not a Plasma applet. A broker library, a
network timeout and a text entry have no business inside the only way a
teacher has of getting anywhere; if this dies the bar is untouched.

The panel mark is Breeze's call-outgoing. The old one was a bell, sitting
two inches from Plasma's notification bell in the same bar and meaning
something entirely different.

Two things it will not do: claim a call was displayed, and let a teacher
type the room. The machine already knows which room it is.

Sixty tests cover the contract without a broker. The live path was proven
against the real coordinator running in the lab, on a loopback broker,
with ProPresenter mocked and the church's networks rejected from inside
the container first.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 10:37:06 -05:00
ekklesiaandClaude Opus 5 380f031092 docs: point at the screenshot index
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 09:13:23 -05:00
ekklesiaandClaude Opus 5 828362b4f4 docs: index the screenshots, and drop the ones that showed nothing
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 09:13:13 -05:00
ekklesiaandClaude Opus 5 ca33669cf7 docs: close the checkpoint with the from-package reproduction
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 09:12:28 -05:00
ekklesiaandClaude Opus 5 d5cc6e463f fix: keep the DVD player whole
Removing gtk-layer-shell from the dependencies broke classroom-dvd on the
first run, and deleting the dock's stylesheet took the DVD control bar's
styling with it. Both go back: the template under a name that says what
still uses it, and the dependency with a line in the package description
explaining that it is there for the disc player and not for a dock.

A probe overlay confirms gtk-layer-shell works under KWin, so the loading
cover survives the move. The focus helper still speaks sway, which is
written down rather than papered over.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 09:10:08 -05:00
ekklesiaandClaude Opus 5 682e3b0552 docs: record what building the package taught
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 09:07:10 -05:00
ekklesiaandClaude Opus 5 f7d5aecd6c build: make the classroom desktop something a .deb installs
Building the package found two things reading the tree did not.

dpkg refused it outright: plasma-workspace already owns
/etc/xdg/plasmanotifyrc and friends, and two packages cannot own one
path. The room's settings move to a directory of their own with
/etc/environment.d putting it in front of /etc/xdg, which is KConfig's
own cascade rather than a file fight.

The postinst then failed on a chmod for a command that no longer
exists. It now walks what the package installed instead of naming
files, removes the login shell that used to start sway on tty1, and
enables SDDM and the audio service.

Verified by installing the built package on a fresh machine and
starting the session from it: restrictions in force, palette applied,
audio service enabled and running, panel supervision unconditional.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 09:06:53 -05:00
ekklesiaandClaude Opus 5 4c3f19d599 build: keep the DVD and MQTT helpers, and count the deletion honestly
Building the package showed what reading the tree did not: classroom-dvd
still calls classroom-window, classroom-mqtt still calls classroom-notify,
and the DVD overlay still imports the icon and stylesheet helpers the dock
shared. They go back in, and the write-up now counts what was actually
removed rather than what could be.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 09:03:26 -05:00
ekklesiaandClaude Opus 5 6e4786f448 build: package the desktop instead of the shell
The install list loses the dock, the window helper, the Chrome window
arbiter, the notification wrapper, the compositor and bar configs and the
GTK icon and stylesheet machinery, and gains a look and feel package, a
colour scheme, window rules, Kiosk restrictions and two supervised user
services. Dependencies follow: Plasma and SDDM in, sway, waybar, mako and
gtk-layer-shell out.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 08:59:56 -05:00
ekklesiaandClaude Opus 5 d87990f0ac docs: write up the classroom v2 result
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 08:54:41 -05:00
ekklesiaandClaude Opus 5 2ff9d47be9 feat: decide the classroom's audio output from what is plugged in
The room that prompted this had a television on HDMI with working PCM
audio and nothing in the analog jack, and WirePlumber chose analog. A
teacher who corrected it by hand then had to restart the browser before
any sound came out.

The policy is a pure function so it can be tested against that room
rather than against a machine: an unavailable port is not a candidate,
an analog port merely existing is not evidence of a cable, and an
explicit AUDIO_OUTPUT overrides the lot. Twelve cases, the real room
among them.

The daemon follows PipeWire's own event stream rather than polling, and
moves streams that are already playing, which is the part that stops a
teacher having to close and reopen the browser.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 08:47:28 -05:00
ekklesiaandClaude Opus 5 87b55761c6 feat: give the classroom a desktop instead of a shell
The whole appliance UI is now configuration for a desktop Debian already
ships: a Plasma 6.3 session with a colour scheme, a look and feel package
that builds the bar, window rules, and KDE's own Kiosk restrictions.

Two things were measured rather than assumed, and both changed a decision.

Plasma restarts a crashed panel three times a minute and then gives up,
which is the failure a classroom has already had. Four lines of systemd
drop-in make it unconditional; the panel then survived eight SIGKILLs a
second apart, back each time in 2.4s.

Plasma draws a minimised task and a background task identically, so the
five states the room needs cannot all be shown. Rather than write a task
manager, the classroom removes minimise: clicking a launcher always means
show me this.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 08:35:12 -05:00
ekklesiaandClaude Opus 5 bfc7caf28f test: a lab that runs a real desktop where no VM can exist
The development host is a VM without /dev/kvm, so a candidate desktop
cannot be given a virtual machine here. It gets a systemd container
instead, with the session nested inside a headless sway so that grim can
photograph it and sway's IPC can aim a real pointer at it.

Plasma 6.3.6 comes up in it, software-rendered, at 1920x1080.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 07:59:46 -05:00
ekklesiaandClaude Opus 5 71db7fd6ec docs: open the classroom v2 architecture experiment
Records the exact stable base commit, the fact that the development host
cannot create a VM, and the first measurement worth having: Debian 13
already ships Plasma 6.3.6.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 07:40:43 -05:00
ekklesiaandClaude Opus 5 aadb1b8819 fix: keep the system print dialog in front of the window that rendered it
The render helper sat on top of the Print dialog, so the dialog could
not be reached and nothing could be printed.

Two causes. The daemon could not see window titles at all — the tree
walk never collected them — and both windows arrive close enough
together to be configured in the same pass, in whatever order the tree
lists them. Configuring a window focuses it, so whichever was done last
ended up in front, and that was not reliably the dialog.

The dialog is now recognised by its exact title and configured last, and
if a Chrome popup takes focus while it exists, focus is handed straight
back. Focus is only ever reclaimed from another popup: clicking the main
browser or the dock is the teacher's business and is left alone.

A title match, narrowly, and only for this one known dialog. Creation
order still decides which Chrome window is the browser; nothing else is
classified by what it happens to be called.

Nothing auto-closes the helper. After Print is cancelled it stays, and
the sticky dock X still closes it.

Proven by looking rather than by state. The render window is given a
saturated background in the fixture so a screenshot answers "which
window is in front" unambiguously: Print opens in front, the helper is
then deliberately raised over it and Print comes back in front,
cancelling Print leaves the helper visible with the dock X still
offering it, and that X closes exactly the helper and leaves the main
browser untouched.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 20:22:53 -05:00
ekklesiaandClaude Opus 5 0e91b4cbd3 fix: keep the dock's Close button reachable
The Close button followed focus, and moving the pointer down to the dock
moves focus off the popup. So the button disappeared somewhere on the
journey to it, and a secondary Chrome window could not be closed with a
mouse at all. Found on the endpoint; my fault.

The target is remembered instead. Once a secondary window has been
focused the dock keeps offering to close it, and losing focus — to the
dock, the main browser, a print dialog, any layer-shell surface —
changes nothing. It moves on only when that window closes or another
secondary takes over, and it falls back to another open secondary rather
than to nothing, so cancelling a print dialog leaves the render window
still closeable. It is chosen from the secondary list, which never
contains the first window, so it can never name the main browser.

Restoring the floating role to a window that already existed no longer
moves or focuses it. Moving it would undo wherever the teacher dragged
it; focusing it would raise the render window over the print dialog
sitting above it, which is one way the print flow was getting disturbed.

Verified by driving a real pointer through the exact path that was
broken: pointer over the popup, click to focus, then travel down to the
dock in steps, screenshotting throughout. The button is still there on
arrival, at 992,1041. Then the print flow: main browser, render window
and a separate Print dialog, pointer to the dock, button still there,
and killing the dialog leaves the render window as the new target with
the main browser untouched.

The button's own action could not be actuated in that harness, because
synthetic pointer input does not reach Wayland clients under headless
sway — the Browser button does not respond to it either. The action was
run directly instead, closing exactly the remembered container and
leaving main Chrome alive.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 20:05:42 -05:00
ekklesiaandClaude Opus 5 b7be59d0dd chore: record classroom 0.1.42 canary
Published as 0.1.42 rather than the intended 0.1.41: update_canary.sh
increments from VERSION, which local builds had already advanced to
0.1.41. The content is the tested tree either way, and going back to a
lower number would leave a canary that nothing already on 0.1.42 would
accept.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 20:30:48 -05:00
ekklesiaandClaude Opus 5 f65c814eff fix: put the popup close button in the dock, and clear the printer location
Three things, none of them redesigns.

The close affordance for a secondary Chrome window was a separate
layer-shell surface anchored to that window's corner, repositioned by a
five-per-second poll. Any position change tore the surface down and
built another, so on real hardware it chased the window and flickered.
Removed entirely: the GTK button process, the geometry maths that placed
it, the --close-button mode that spawned it, and the poll loop that
drove it. The window helper no longer imports GTK at all.

The behaviour underneath it was right and is kept. The first Chrome
top-level is the classroom browser, tiled and borderless; later ones
float centred at about three quarters of the output. Creation order is
the only thing that can tell them apart — confirmed on a real session,
where every Chrome top-level shares app_id, shell, PID and a null
transient_for, and sway exposes no window properties for Wayland
toplevels at all. Titles are still not used. It now follows sway's own
window events instead of polling, so dragging a popup costs nothing.

Close moved into the dock, which was already on screen and does not
move, so it cannot chase anything. It appears only while a secondary
window is focused and runs kill on that one container. The helper
publishes which window that is; the dock watches the file rather than
polling. A stock sway title bar has no close button, so restoring the
border would not have solved this either.

Two bugs found by looking rather than by testing state. Chrome
renegotiates the size of a popup opened with explicit dimensions after
the resize returns, which left windows centred for a width they no
longer had; placement now settles and repeats once, at creation only, so
dragging is still free. And the dock's Close button came up empty
because show_all does not recurse into a no-show-all widget.

The printer's Location is now blank. CUPS prints it under the queue name
in every dialog, so "Ekklesia Classroom" was a second line the teacher
had to read past; the identity is one word. The already-configured check
includes Location, so a machine set up before this is corrected rather
than left as close enough.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 20:24:51 -05:00
ekklesia a1d8150d7c Run provisioning after package install 2026-09-06 18:33:48 -05:00
ekklesia 1e9ca24d57 Release 0.1.37 deployment prep 2026-09-06 18:17:04 -05:00
ekklesia 7f66228b9e Release 0.1.37 deployment prep 2026-09-06 17:30:30 -05:00
ekklesiaandClaude Opus 5 4862e29f57 chore: record classroom 0.1.36 canary
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 14:42:05 -05:00
ekklesiaandClaude Opus 5 dfd42f6fdf feat: give the rooms a line to the church broker
The first piece of turning these machines into connected endpoints.
classroom-mqtt is one small service per room that subscribes, validates
what arrives, and hands the work to commands that already exist.

Nothing in the room depends on it. A broker that is down, a network that
is missing, a machine with no broker configured at all — each is a
service sitting quietly, not a classroom that fails. Nothing waits for
it at boot and the shipped default is off, so every existing classroom
upgrades into this release with messaging disabled until a broker
exists.

The room is the hostname, which is already how these machines are named.
Topics carry no product name because the broker is the church's and
these are just rooms: classroom/notify/all, classroom/notify/<room>, and
a retained classroom/status/<room> so anyone asking later can be told
which rooms are up without waiting for them to say so again.
Notifications are events and are never retained; status is state and is.

Stale messages cannot come back, which is the property that matters most
and has two halves. The session is clean, so a broker never hands over a
backlog from while the room was off. And retained notifications are
refused on arrival, because otherwise one would replay into every room
at every reboot forever. The honest limit is written down: MQTT only
flags a retained message when it is replayed to a new subscription, so a
retained publish delivered live is indistinguishable from an ordinary
one and is shown once — which is right, since it is live at that moment,
and the replay is the actual hazard.

Delivery reuses the session-aware sender rather than a second copy of
it. classroom-notify-test grew into the real thing, so it is now
classroom-notify with the sample sender a wrapper over it, and its 36
tests came along.

The broker password lives in device.conf, so that file stops being world
readable: 0640 root:classroom, which everything that reads it already
satisfies.

Transport logic is kept free of any broker library so it can be tested
without one — 40 checks on room identity, the topic contract and which
payloads become notifications. Separately verified against a real
mosquitto: connect, subscribe, retained status, per-room and all-room
delivery, urgency, duplicate and expired and malformed messages ignored,
the broker killed with the daemon surviving, reconnect, and a stale
retained notification left on the broker not reaching the screen when a
room boots.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 14:41:49 -05:00
ekklesiaandClaude Opus 5 c3106edd30 chore: record classroom 0.1.35 canary
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 14:19:59 -05:00
ekklesiaandClaude Opus 5 a1a218b29f fix: draw classroom icons at the display's real pixel density
Icons went soft under UI_SCALE while the clock text beside them stayed
sharp, which is visible on a 24" monitor even if it hides on a TV.

new_from_file_at_scale rasterises an SVG at its *logical* size. The
artwork is vector, but by the time sway scaled the session it was
already a fixed raster, and the compositor enlarged that. Text never had
the problem because GTK draws it at the real density.

GTK 3 has no fractional scaling, so sway advertises the ceiling integer
scale and scales the finished buffer down. Measured under sway: scale
factor 1 at output scale 1.0, and 2 at 1.25, 1.5 and 2.0. So each icon
is now rasterised at size x scale_factor physical pixels and handed to
GTK as a cairo surface carrying that device scale. The icon lands at the
real density; its logical size, and the whole dock layout, is unchanged.

Icons redraw on notify::scale-factor as well as at realize, since the
scale factor is not known until a widget is on a display and changes if
UI_SCALE is changed under a running session.

Browser, DVD, LIVE and all six DVD control icons go through it, in a
shared module so the dock and the DVD session cannot diverge. The
throbber is drawn rather than loaded and had the same flaw, so it draws
on a device-scaled surface too rather than being the one soft thing left
in a sharp dock.

The artwork is untouched. Verified by rendering the dock under sway at
1.0, 1.25, 1.5 and 2.0 against the previous build: apparent icon size is
identical at every scale (28, 35, 42, 56 px), 1.0 is unchanged
pixel-for-pixel, and peak edge contrast on the Chrome icon goes from 60
to 99 at 1.25, 49 to 83 at 1.5, and 37 to 82 at 2.0 out of 255. The DVD
control bar was checked the same way.

Also records that Wonder Ink session-only cookies are confirmed working
on the endpoint, which settles the question 0.1.34 shipped as unverified
and tells us the session is in cookies rather than localStorage.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 14:19:38 -05:00
ekklesiaandClaude Opus 5 a0f2b74d05 chore: record classroom 0.1.34 canary
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 08:27:49 -05:00
ekklesiaandClaude Opus 5 0b2f99a853 feat: make Chrome a room appliance rather than somebody's browser
A classroom computer is shared. There is one local Chrome profile owned
by the room, teachers sign into websites, and nobody signs into Chrome.

BrowserSignin=0 stops Chrome asking who is using it, with SyncDisabled
set explicitly alongside it so the intent is in the file rather than
implied. BrowserAddPersonEnabled=false stops new profiles and
ProfilePickerOnStartupAvailability=1 stops the picker.

BrowserGuestModeEnabled=false is the answer to the other profile
switching entry point. Guest is the one that survives disabling add
person, and turning it off is the supported mechanism rather than hiding
UI. Guest is not the browsing model: the room needs one persistent
profile so the church YouTube login survives.

CookiesSessionOnlyForUrls scoped to [*.]wonderink.org, which covers
app.wonderink.org and every other subdomain, so a teacher's Wonder Ink
session does not outlive them. BackgroundModeEnabled was already false,
which is what lets a session actually end.

Every policy name was checked against the strings of the installed
Chrome 152 binary, with deliberately invalid names as a control so the
check is known to discriminate.

Two things are recorded honestly in docs/browser.md rather than claimed.
The avatar button stays in the toolbar — screenshotted with and without
these policies, it is in the same place either way; what changes is that
nothing behind it can create or switch a profile, and no Chrome 152
policy removes the button. And whether session-only cookies actually
sign Wonder Ink out is unverified: it needs a real Wonder Ink login,
which this machine does not have, and three attempts to prove the
mechanism against a local site were inconclusive because the positive
control never worked. The endpoint procedure to settle it, including how
to tell a cookie session from a localStorage one, is written down.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 08:27:16 -05:00
ekklesiaandClaude Opus 5 b83f5b7aed chore: record classroom 0.1.33 canary
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 00:27:03 -05:00
ekklesiaandClaude Opus 5 8ba04801a7 docs: record the Wonder Ink Present view observation
Vimeo plays in the normal lesson view and stops immediately in Present
view, which rules the machine out: same video, same host, same codecs, a
click earlier. No packages were installed and nothing was changed.

Written as a decision procedure rather than a shrug. The first step is a
stock Chrome with no classroom policy and a clean profile, which settles
in one run whether this is Wonder Ink or something at our end. Two
candidates follow if it turns out to be ours, each with the diagnostic
that would confirm it, and the note that AutoplayAllowlist is recognised
by Chrome 152 but has deliberately not been applied — allowing autoplay
on a classroom machine should follow evidence rather than a guess.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 00:21:18 -05:00
ekklesiaandClaude Opus 5 d76e911612 feat: acknowledge a dock click that takes a moment
Clicking Browser and watching nothing happen for several seconds is the
one place the classroom felt broken rather than quiet. The button now
becomes a throbber the instant it is pressed, and stays one until the
window actually appears in the sway tree.

Only a real launch spins. The dock asks sway whether Chrome is already
open first, so focusing or restoring an existing window stays immediate,
which is how it should feel. DVD is left alone: it has its own
full-screen loading surface and does not need two answers to one click.

A launch that never arrives gives up after thirty seconds, puts the icon
back and says so, so a broken start cannot spin all morning.

The throbber is drawn rather than a GtkSpinner: the stock one is capped
well below the size of the icon it replaces, so it read as a hesitation
rather than as work in progress. Pre-rendered frames swapped into the
same Gtk.Image keep the button an ordinary image with ordinary clicks,
and it falls back to GtkSpinner if cairo is ever missing.

The pointer also turns busy while it is over the dock. Wayland gives the
cursor to whichever client the pointer is over and sway offers no way to
set one globally, so this only holds on our own surface — which is
exactly where the pointer is at the moment a dock button is pressed.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 00:21:18 -05:00
ekklesiaandClaude Opus 5 255072ac94 feat: settle Chrome's window, crash bubble and password prompts
Three things a volunteer should never have to see.

Sway was drawing its own title bar above Chrome's tab strip, which
already carries the window controls. Verified on a real session: Chrome
is an xdg_shell client with app_id "google-chrome" and sway was giving
it border "normal". A scoped rule removes the compositor decoration
only — Chrome keeps its tabs and address bar, stays a normal window, and
still maximises and minimises. A class rule covers an XWayland fallback.
Nothing else changes: the only other window in this classroom is VLC,
which is already borderless, so no broader rule is warranted.

"Restore pages? Chrome didn't shut down correctly" is suppressed with
--hide-crash-restore-bubble. Note the name. The switch this was going to
use, --disable-session-crashed-bubble, is not present in Chrome 152 at
all — checked against the strings of the installed binary — so it would
have silently done nothing.

That hides the prompt; the profile was still being marked as crashed,
which is the untidy half. Chrome's browser process shuts down cleanly on
SIGTERM, so a shutdown unit now gives it one and waits up to eight
seconds before the session is torn out from under it. Best effort by
construction: a shutdown that will not finish is far worse than a crash
flag.

Password prompts go off with the PasswordManagerEnabled policy, on the
path the classroom already manages. Existing profile and session data
are untouched, and website logins are unaffected: Wonder Ink and YouTube
stay signed in.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 00:21:02 -05:00
ekklesiaandClaude Opus 5 890e365d81 chore: record classroom 0.1.32 canary
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 23:17:58 -05:00
ekklesiaandClaude Opus 5 8bed3d186a feat: scale the whole session, not just the classroom's own windows
UI_SCALE only ever reached the surfaces we draw ourselves. Chrome stayed
tiny — its tabs, omnibox and menus are Chrome's own pixels, and Ctrl+
only scales page content, which is no use to a volunteer reading a
toolbar from across the room.

UI_SCALE is a property of the television, not of one program, so it now
sets sway's output scale, which every client is measured against.
classroom-scale runs first in the session and applies it. The display
mode is untouched: the panel stays at 1920x1080 and only the logical
coordinate space shrinks, which is what makes everything in it larger.

Our own surfaces stop scaling themselves. They are measured in the same
logical pixels sway is already scaling, so applying UI_SCALE again would
double it. SHELL_SCALE is that decision in one named place, and setting
it back to ui_scale(config) would restore per-surface scaling without
touching a template.

Measured at 1920x1080 with Chrome 152. Chrome's tab strip 40 -> 49 ->
59px against 1.0, 1.25, 1.5; sway's title bar 25 -> 31 -> 38px; the
classroom dock 74 -> 92 -> 110px. Everything scales once, together, and
1.0 is identical to no scaling at all.

Two tradeoffs, both measured rather than assumed. GTK 3 has no
fractional scaling, so at 1.5 it renders at 2x and sway scales down —
very slightly softer, invisible at classroom distance. XWayland is
softer too: terminal text at 1.5 showed about 50% more intermediate-tone
pixels than at 1.0. VLC is the only XWayland application here, and in
DVD mode none of its interface is on screen, only video from a 720x480
disc that is being upscaled regardless. Chrome is a native Wayland
client and stays sharp, which is what matters for reading.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 23:17:37 -05:00
ekklesiaandClaude Opus 5 37f8166401 fix: hold the DVD cover until the disc menu is actually showing
The readiness condition was playback state alone, and on a real disc
VLC reports playing well before its window has taken the screen. The
cover came off onto VLC's ordinary tiled startup window — black, sized
to everything except the dock's reserved strip — and the disc menu only
arrived afterwards. Exactly the state the cover exists to hide.

Ask sway instead. VLC only takes the display when its window goes
fullscreen, and the tree says when that happens. The cover now comes off
when VLC reports playing or paused, its window is fullscreen, and 800ms
have passed for the menu to draw its first frames. The playback check
runs first because it is local and cheap; the tree is only walked once
VLC claims to be playing something.

The twenty second deadline is unchanged: a cover with nothing behind it
is still the one outcome DVD mode must never produce.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 23:17:37 -05:00
ekklesiaandClaude Opus 5 719b856040 refactor: share the sway helpers
classroom-dvd-session needs to ask sway whether VLC has taken the
screen, which is the same tree walking classroom-window already does.
One copy, in a module both import; classroom-window loses a third of its
length.

The state machine is worth protecting through a move like that, so its
regression test comes with it: the exact swaymsg commands for launch,
focus, minimise, restore, Super+M, Super+Shift+M, the scratchpad rescue
and sway not running at all.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 23:17:18 -05:00
ekklesiaandClaude Opus 5 86dff744f7 chore: record classroom 0.1.31 canary
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 22:52:39 -05:00
ekklesiaandClaude Opus 5 68b6347c05 fix: cover the whole display while a disc loads
The loading surface was invisible. GtkWindow only renders its own CSS
background when the application is not painting the window itself, and
this window was created app-paintable with no draw handler and no child
— so it drew nothing at all. The black area during loading was never it:
it was VLC's own window, tiled by sway into the usable area, which
excludes the strip the dock reserves. That is why the dock and the
wallpaper stayed on screen underneath.

An earlier headless check missed this because that sway had no wallpaper
configured, so an invisible surface and a black one looked identical.
Rechecked with the real wallpaper and the real dock underneath.

The surface now paints, and carries a spinner and "Loading DVD..." on
the same translucent rounded card as the dock. It moves from the top
layer to the overlay layer, because the dock is a top-layer surface and
DVD mode has to cover it rather than depend on which surface sway
created first. Its exclusive zone of -1 keeps sway from moving it out of
the way of the dock's reserved strip.

It goes up before VLC is started rather than alongside it, so no part of
VLC's window is ever seen against the wallpaper, and comes down only
once VLC reports playback on screen. The deadline that takes it down
regardless is now twenty seconds, since an optical drive can be slow,
and a failure to start VLC at all ends the session instead of leaving a
cover with nothing behind it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 22:52:21 -05:00
ekklesiaandClaude Opus 5 4a0ff43253 chore: record classroom 0.1.30 canary
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 22:34:12 -05:00
ekklesiaandClaude Opus 5 f8ddfbe345 feat: make DVD playback a classroom surface, not a media player
Pressing DVD now enters DVD mode rather than opening VLC. VLC is still
the backend, because its dvdnav module is what decodes disc menus and
nothing else on Debian does that reliably, but nothing a person can see
or press belongs to it any more.

classroom-dvd-session owns the whole thing. It puts a full-screen black
layer-shell surface up the moment the button is pressed, starts VLC
behind it, and drops it once playback is actually on screen — so the
harsh cut from wallpaper to VLC's black window is gone, and no partial
VLC window or reserved dock strip is ever visible. A deadline drops the
surface regardless, because DVD mode must never leave a black screen
with nothing behind it.

The controls are a classroom bar on the overlay layer, the one layer
sway draws above a fullscreen window. It slides up when the pointer
reaches the bottom of the screen and hides again shortly after it
leaves. It is drawn from the dock's own stylesheet at the room's UI
scale, so it is the same surface, the same radius and the same white
icons. While hidden it only accepts the pointer in a thin strip along
the bottom edge, so a click on a disc menu goes to the disc.

The bar reaches VLC through an HTTP interface on loopback behind a
password generated per session, sending hotkey *actions* by name. That
is what makes the next part possible.

All 108 hotkeys VLC 3.0.23 defines are unset, then Space, Esc and the
arrow keys plus Enter are bound. Crop, aspect ratio, subtitles, audio
tracks and deinterlacing are no longer reachable by pressing a key at
random, and the arrow keys no longer change VLC's volume — the
television owns volume, and CEC will own it properly later.

Esc quits instead of leaving fullscreen. There is nothing to leave, and
leaving it was what stranded people on a black VLC window. Close, Eject
and Esc all ask VLC to quit and escalate to SIGTERM then SIGKILL if it
does not; when VLC exits by any route, including a crash, the session
ends and the classroom desktop comes back.

Verified against VLC 3.0.23 on Trixie: every argument accepted, every
control action accepted over the control interface, a wrong password
refused, and quit exiting cleanly. The surfaces were rendered under a
headless sway and screenshotted to check the bar against the dock.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 22:33:53 -05:00
ekklesiaandClaude Opus 5 3e69f3f05c refactor: share the stylesheet renderer
The DVD control bar has to look like the dock, which is best guaranteed
by drawing it from the same template with the same measurements rather
than by keeping two stylesheets in step by hand.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 22:33:37 -05:00
ekklesiaandClaude Opus 5 35dd742211 chore: record classroom 0.1.29 canary
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 21:34:07 -05:00
ekklesiaandClaude Opus 5 a3dba6e8fb feat: provision Chrome and DVD codecs without a manual step
A fresh classroom needed two things installed by hand on every box:
Google Chrome, which is not in Debian, and the DVD codecs, which Debian
cannot ship. Both now happen on their own.

ekklesia-classroom-provision.service runs after every boot and does the
work that needs the network and the apt lock, neither of which a package
postinst can have. Each step returns immediately if it is already done,
so a provisioned machine costs nothing and a machine that was offline
the first time gets another go at the next boot. Nothing there is
allowed to fail the boot: a classroom with no DVD codecs is still a
working classroom, and it says so in the log rather than silently.

The postinst enables the service but deliberately does not start it.
Every OTA update reboots the moment the install finishes, and an apt run
killed by that reboot would leave dpkg half-configured.

Chrome comes from Google's own repository rather than a one-off .deb, so
it keeps getting security updates. Google's package migrates whatever we
write into a deb822 file of its own, so the repository is looked for
across every source file instead of only the one we would write;
otherwise a machine that had Chrome removed would end up with two copies
of the same repository.

The DVD codec helper previously assumed deb822 sources and did nothing
on a machine using a classic /etc/apt/sources.list, which is what the
hardware actually runs. It now handles both formats, and only ever adds
a missing component to an existing Debian entry — so it cannot produce a
duplicate repository or a duplicate component however many times it
runs, and third-party repositories like Chrome's are never touched. It
also answers libdvd-pkg's debconf questions, so nothing blocks on a
prompt in a service with no terminal.

Tested against fixture APT trees for classic sources.list, deb822
.sources, contrib already enabled, repeated runs, third-party
repositories and the awkward real lines: bracketed options, indentation,
trailing comments and flat repositories.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 21:33:44 -05:00