Commit Graph
130 Commits
Author SHA1 Message Date
ekklesiaandClaude Opus 5.5 6c04f743ed test: every rehearsal installs with apt and proves which version landed
The dirty-upgrade lab installed the new package with dpkg -i. Since
0.2.6.2 the package conflicts with khelpcenter, which dpkg refuses and apt
resolves, so the 0.2.2 rehearsal left the machine on 0.2.2 and asked every
question of the old release. Old package, new package and the reinstall
now all go through apt, as ekklesia-classroom-update does, and each stops
the run unless dpkg reports exactly that version, fully installed. The
container gate asserts the same of its previous release and its reinstall.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:05:54 -05:00
ekklesiaandClaude Opus 5.5 9fc86f9d4c test: the wallpaper repair, on Tiny Pebbles' own file, in the lab and on a real install
Unit: Tiny Pebbles' file (desktop 10, panel 1), a healthy River Kids file
left byte for byte, a late desktop beside a good one, idempotence, the
refusal under a running shell. Lab: Plasma makes a bare desktop itself
through a new activity, and the screen is read, not the file. Container
gate: the installed package's fallback is the package, and its repair
fixes the room's file and leaves the panel alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:05:54 -05:00
ekklesiaandClaude Opus 5.5 7b4ac39cd2 fix: a desktop Plasma makes after the layout has run gets the classroom's wallpaper
Tiny Pebbles came through the 0.2.2 upgrade on Plasma's own purple
wallpaper: its desktop was containment 10, made after the layout had run
(most likely the television arriving after a night-time reboot), with no
wallpaper configured. The look and feel's fallback could not catch it,
because it named a file and Plasma only accepts a wallpaper package there.

The fallback is now the EkklesiaClassroom package, whose one image links to
the classroom's picture. classroom-desktop-wallpaper, run before every
plasmashell start, writes the picture into every desktop containment the
file already has: found by plugin, never by number; panels and everything
else byte for byte; a file that is already right not written at all.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:05:44 -05:00
ekklesiaandClaude Opus 5.5 530f6f6563 release: 0.2.6.5, for the canary machine only
An update reboots the computer only once nobody has used it for half an
hour and nothing is playing, instead of the moment it is installed. The
update check stays daily at 03:00 local time, Persistent.

Published to canary: ekklesia-classroom_0.2.6.5_amd64.deb, sha256
48919410492d398d103c2fd2e4e5bd0a97761e7032f9438fbb08f1a29c045050, gated
as a fresh install and as an upgrade from 0.2.6.4 (which stable serves),
and rehearsed separately as the upgrade from 0.2.2. Upgraded live in the
lab 0.2.6.2 -> 0.2.6.4 -> 0.2.6.5: the timer stayed enabled, daily at
03:00, Persistent, with its last-check record kept. Stable still answers
0.2.6.4, promoted earlier today; nothing here wrote to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 23:10:26 -05:00
ekklesiaandClaude Opus 5.5 b2cde1f414 docs: when a classroom updates, and when it reboots
Daily at 03:00 local time spread over half an hour, caught up once after a
night switched off, on the device's own channel only; and since 0.2.6.5 a
reboot that waits for half an hour with nobody using the computer and
nothing playing. With the journal commands to see each, and the checks
only real hardware can make.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 22:56:08 -05:00
ekklesiaandClaude Opus 5.5 7586326902 test: the lab measures the update schedule on its own systemd, powered off and on
Switched on at the time: the timer fires once and is next due tomorrow.
Switched off: the lab machine is really stopped across the time and
started after it; the missed check runs once, 50 s after starting, and
never loops. And the reboot-when-idle watcher against real kernel input,
a mouse made with /dev/uinput: no reboot while it moves, a reboot once it
is left alone. 11 checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 22:56:08 -05:00
ekklesiaandClaude Opus 5.5 3c40e849ea fix: an update reboots the computer only once nobody is using it
The update check has been daily at 03:00, Persistent, since 0.1.5 — the
lab's installed stable machine logged it at 03:01 and 03:04 on successive
days — and it stays so. What changes is the reboot after an update. It
happened the moment the update was installed: harmless at 03:00, and a
room restarting itself mid-morning when a computer switched off at 03:00
caught up after being switched on.

Now the updater installs, with every check it had (the device's own
channel, a newer version, the manifest's SHA-256, name and version), and
starts classroom-reboot-when-idle, which reboots once no input device has
produced an event for 30 minutes and no sound card has played for as
long. At 03:00 that is about 03:30; switched off first, the next start is
on the new version. The service has no [Install] and nothing enables it.

The release gate checks after every install and upgrade that the timer is
enabled, daily at 03:00, Persistent, with no boot or repeat trigger and
no drop-in, and that the updater no longer reboots by itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 22:56:08 -05:00
ekklesiaandClaude Opus 5.5 23194a085c release: 0.2.6.4, for the canary machine only
Temporary workspace restore: Wonder Ink brings back the teacher's recent
tabs for two hours after they last used it — never the sign-in, which
WonderInk keeps as a Basic-auth password and which is not stored. The
admin-only link bubble is documented, not overridden.

Published to canary: ekklesia-classroom_0.2.6.4_amd64.deb, sha256
80388e7ac3d9a1a266ebc3454f64808b6a0e2cd75b06fa934caf9f2a25026253, gated
on that file as an upgrade from 0.2.6.3 and from 0.2.2. 0.2.6.3 and
0.2.6.2 are on the server as they were; latest.deb and the manifest read
back over HTTPS as 0.2.6.4. Stable still answers 0.2.2; nothing here
wrote to it. No desktop is affected.

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 21:12:17 -05:00
ekklesiaandClaude Opus 5.5 78a844968c test: the lab restores real tabs across real launches, and follows real links
tools/lab/wonderink-workspace-check launches the installed command again
and again against a stand-in for WonderInk's start page whose sign-in is
Basic auth, as WonderInk's is: restored after 5 and 90 minutes; fresh
after two hours and a minute, a day and a week; reopened untouched until
it lapses; a click after a restore counts and the restore does not; a
busy page and an untouched print preview do not; Print does; closed,
reordered, worksheet and missing-worksheet tabs; killed outright; a
restore that crashes twice; corrupt, future-format and half-written
files; the file holding no credential; restored tabs printing. 59 checks.

The print check follows every kind of link on a signed-in page with one
click — new tab in front for links and window.open, nothing for buttons
and anchors, same-tab for no target, Back, closing — and prints a PDF and
a picture reached by a link, and a page after several tabs. 0.2.6.3's
scenarios are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 21:12:17 -05:00
ekklesiaandClaude Opus 5.5 b5d9535467 feat: Wonder Ink brings back recent tabs for two hours, never the sign-in
A teacher who closes Wonder Ink by accident, or whose computer crashes or
restarts, gets their recent tabs back if they open it again within two
hours of last using it. Later than that — overnight, next week — Wonder
Ink starts fresh and the file is removed.

It is a temporary workspace restore, not a sign-in. Measured on the real
site: WonderInk's rrf-auth cookie is "Basic " and the base64 of the
teacher's email and password, and its start-up authenticates again on
every load; keeping a sign-in would mean keeping a password. Nothing of
it is kept — no cookie, header or storage — and closing Wonder Ink still
signs the teacher out. What is kept is each tab's kind, address or
worksheet file, and title; the order; the tab in front; the time of last
use. An address with a credential in it is not kept at all.

Every WonderInk page has one address, so its tabs come back as one, at
the sign-in page. Its resource files are public and come back as they
were. A worksheet comes back only from Downloads/Wonder Ink, and only if
it is still there and still a PDF. The tab in front loads at once, the
rest when first shown: measured, a PDF loaded in a hidden tab showed at
10%.

Using it is the teacher's input — click, key, wheel, touch — never a
page's own activity, a print preview left open, or the restore itself,
so reopening cannot keep a workspace alive. A restore that crashes twice
is abandoned. Written atomically when tabs change and at most every 30 s
for the time of use; unreadable, future-format and clock-gone-back files
start fresh. The keeper stops filtering events at aboutToQuit: left in
place it crashed Wonder Ink on exit one run in several; 30 runs, none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 21:12:17 -05:00
ekklesiaandClaude Opus 5.5 5fef4e02a9 release: 0.2.6.3, for the canary machine only
Black and white is shown in the preview as it prints, and CUPS is given
the file shown; right-click on a picture in a lesson offers Print image,
which prints that picture with the picture layout, and Print page.

Published to canary: ekklesia-classroom_0.2.6.3_amd64.deb, sha256
2b8f26409b5374f08e8d461d8c711307d5180c3b120654df0150a587b7d64710, gated
on that file as an upgrade from 0.2.6.2 and from 0.2.2. 0.2.6.2 is still
on the server as it was. Stable still answers 0.2.2; nothing here wrote
to it. No desktop is affected.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 16:47:56 -05:00
ekklesiaandClaude Opus 5.5 f9a0ee08f9 docs: 0.2.6.3 — what was measured, the manual, and Sunday's checks
Why black and white is made with Ghostscript and how close it is to what
Elmer prints; why Print image goes through the engine's copy image, with
the table of what each other way failed on; what it cannot reach (a CSS
background picture) and what it costs (the clipboard lives in Wonder Ink
afterwards). The manual says Print page and Print image, and that the
preview turns grey. Sunday's paper checks for both, in testing.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 16:34:38 -05:00
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