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]>
@@ -1,6 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Ekklesia maintenance terminal
|
||||
Name=Maintenance terminal
|
||||
Comment=For the technical administrator, standing at the machine
|
||||
Exec=/usr/local/bin/classroom-admin-terminal
|
||||
Icon=utilities-terminal
|
||||
|
||||
@@ -38,6 +38,27 @@ action/help_contents=false
|
||||
# that has no settings application anyway; the lock belongs on the
|
||||
# restrictions above, not on the palette.
|
||||
[General]
|
||||
# Sized for a television, not a desk.
|
||||
#
|
||||
# Everything Plasma draws is measured in grid units, and a grid unit is
|
||||
# the height of this font: the notification card's width, its padding,
|
||||
# its icon and its buttons all come from here. There is no setting that
|
||||
# makes a notification larger on its own, and the supported lever is this
|
||||
# one — which is also the reason it is used rather than a replacement
|
||||
# notification plasmoid.
|
||||
#
|
||||
# Measured at 100% in the lab: Breeze's 10 point gives a card about 350
|
||||
# by 110 pixels with title text a volunteer at the back of the room
|
||||
# cannot read; 15 point gives about 410 by 145 with a title that carries.
|
||||
# The bar does not change, because its height is fixed and the clock
|
||||
# sizes itself to it.
|
||||
#
|
||||
# At 200% the display scale doubles all of it again, so a 4K television
|
||||
# gets the same card at twice the size without a second number.
|
||||
font=Noto Sans,15,-1,5,50,0,0,0,0,0
|
||||
smallestReadableFont=Noto Sans,13,-1,5,50,0,0,0,0,0
|
||||
menuFont=Noto Sans,15,-1,5,50,0,0,0,0,0
|
||||
toolBarFont=Noto Sans,15,-1,5,50,0,0,0,0,0
|
||||
ColorScheme=EkklesiaDark
|
||||
AccentColor=79,179,168
|
||||
# The teacher's own words are not decoration: keep text rendering plain
|
||||
|
||||
@@ -1201,6 +1201,98 @@ worked: it used `iptables`, which is not installed in the lab image, and
|
||||
it probed with `/dev/tcp` under `sh`, which is a bash feature and fails
|
||||
under dash whatever the network is doing. Both were reported as success.
|
||||
|
||||
## A morning in the rooms: 0.1.50
|
||||
|
||||
Four things a television found that a container had not.
|
||||
|
||||
### "Parent Call has not been set up yet", on a room that was
|
||||
|
||||
The card came up checking with the sanctuary and then said the room had
|
||||
never been set up. Nothing about the room was wrong. Read from the church
|
||||
broker, read-only:
|
||||
|
||||
```
|
||||
classroom/parent-call/coordinator {"online": true, ...}
|
||||
classroom/parent-call/pending {...}
|
||||
classroom/parent-call/mode — absent
|
||||
```
|
||||
|
||||
The coordinator's own source says why. It publishes the retained mode
|
||||
only in answer to an operator writing to `…/mode/set`, and on this
|
||||
broker nobody ever has. The classroom waited 1.5 seconds for a retained
|
||||
message that will never exist, and then told the teacher the room was
|
||||
unconfigured.
|
||||
|
||||
So a missing mode is now read as what it is: the coordinator's own
|
||||
default, which the sanctuary end knows and this end does not. The room
|
||||
offers the call and lets the coordinator answer. If the true mode is off,
|
||||
the coordinator refuses in its own words through the status topic, which
|
||||
the card already shows — a wrong no costs a teacher the call, a wrong yes
|
||||
costs a round trip and tells the truth at the end of it.
|
||||
|
||||
The distinction the card has to keep is untouched. A broker that is not
|
||||
configured at all still says so, a broker that cannot be reached still
|
||||
says that, and a coordinator that is down still names the sanctuary.
|
||||
"Undecided" is now a third answer, with no message, which is why the card
|
||||
shows nothing at all until the retained topics have had their chance.
|
||||
|
||||
Proved on a rig whose broker was made to match the church's exactly —
|
||||
coordinator and pending retained, mode deleted. The card came up ready,
|
||||
and a call went through to the sanctuary mock: `Ruth Alvarez`, `Nursery`.
|
||||
|
||||
### The card opens where its button is
|
||||
|
||||
Centred, on a classroom television, is a long way from the button in the
|
||||
bottom-left corner. A Wayland client cannot place its own window, so the
|
||||
card is anchored rather than positioned: layer shell, bottom and left
|
||||
edges, margins in logical pixels. The compositor does the arithmetic, so
|
||||
it is correct at 1920×1080 at 100% and at 3840×2160 at 200% with no
|
||||
coordinates written down anywhere. Both were photographed.
|
||||
|
||||
The classroom has drawn the disc player's cover this way under KWin for
|
||||
months, so the mechanism is not new here. It is optional: if the library
|
||||
is missing the card still opens, centred, exactly as before.
|
||||
|
||||
**The one thing the lab cannot answer is whether a teacher can type into
|
||||
it.** The entry takes focus and shows a caret, which is KWin giving the
|
||||
layer surface the keyboard. Actually typing has never been testable in
|
||||
this lab — the tool that tries closes the card — so the first bench test
|
||||
on this build is a name typed into Parent Call with a real keyboard.
|
||||
|
||||
### Notifications for a room, not a desk
|
||||
|
||||
Everything Plasma draws is measured in grid units, and a grid unit is the
|
||||
height of the general font. There is no setting that makes a notification
|
||||
larger on its own, so the font is the lever — and it is the supported
|
||||
one, which matters after the shell package that cost the panel.
|
||||
|
||||
Measured at 100%: Breeze's 10 point gives a card about 350×110 with a
|
||||
title a volunteer at the back cannot read; 15 point gives about 410×145
|
||||
with a title that carries. Text, icon, padding and buttons all move
|
||||
together because they are all grid units. The bar does not change: its
|
||||
height is fixed and the clock sizes itself to it.
|
||||
|
||||
At 200% the display scale doubles all of it again. No chime, unchanged:
|
||||
`AudioFeedback` is off in `plasmaparc` and the applet's own setting is
|
||||
off in the layout.
|
||||
|
||||
### The room does not introduce itself
|
||||
|
||||
"Ekklesia Classroom" is gone from everything a teacher sees. The
|
||||
notification card is from `Classroom`, the disc player's notifications
|
||||
are from `DVD`, the maintenance terminal is a `Maintenance terminal`.
|
||||
Nineteen visible names are checked by the test suite now, and the product
|
||||
name stays where it belongs: package names, units, paths and topics.
|
||||
|
||||
### And the lab, again
|
||||
|
||||
Four rooms named after container ids are sitting on the church broker as
|
||||
retained status messages. The fence was applied when each container
|
||||
started and lost the moment it was restarted, because routes do not
|
||||
survive that — and `lab reboot` is in every scenario. It re-fences now
|
||||
and refuses to continue if either church network answers. The four
|
||||
retained topics are the user's to clear; the commands are in the report.
|
||||
|
||||
## Unresolved risks
|
||||
|
||||
In the order I would worry about them.
|
||||
@@ -1275,7 +1367,13 @@ In the order I would worry about them.
|
||||
updates at all, and `classroom-migrate` moves every upgraded machine
|
||||
onto that channel — deliberately, because stable and canary would
|
||||
both hand a Plasma room the sway build back.
|
||||
17. **Ctrl+Alt+T is still registered.** Konsole's own entry declares it
|
||||
17. **Nobody has typed into the anchored Parent Call card.** Layer shell
|
||||
gives it the keyboard — the entry takes focus and shows a caret — but
|
||||
keyboard injection has never worked in this lab, so the first thing
|
||||
to do on the bench is type a name into it. If it cannot be typed
|
||||
into, removing gtk-layer-shell from the package restores the centred
|
||||
card with no other change.
|
||||
18. **Ctrl+Alt+T is still registered.** Konsole's own entry declares it
|
||||
and the registration survives being shadowed. It is inert, because
|
||||
Konsole refuses to open a shell under the classroom's
|
||||
`shell_access=false` — measured — but it rests on that one
|
||||
|
||||
@@ -101,6 +101,16 @@ container lab. See [../classroom-v2-experiment.md](../classroom-v2-experiment.md
|
||||
| `pc-unreachable`, `pc-not-configured` | Parent Call telling a broker it cannot reach from one that was never set up |
|
||||
| `dvd-absent-bar` | the bar on a machine with no optical drive |
|
||||
|
||||
## A morning in the rooms
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `pc-no-mode` | Parent Call ready against a broker with no retained mode — the state that used to say the room was never set up |
|
||||
| `pc-bottom-left`, `pc-200-package` | the card anchored bottom-left at 100% and at 200% |
|
||||
| `notif-package-100`, `notif-package-200` | the notification card at both scales, from `Classroom` |
|
||||
| `notif-13pt`, `notif-15pt` | the two font sizes that were weighed for it |
|
||||
| `pc-typed-layer` | the card gone: the lab's keyboard tool closes it, which is why typing is still a bench test |
|
||||
|
||||
## Failures, kept
|
||||
|
||||
| | |
|
||||
|
||||
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 525 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 541 KiB |
|
After Width: | Height: | Size: 282 KiB |
@@ -287,12 +287,18 @@ def broker_message(why=None):
|
||||
return BROKER_WORDING.get(why, BROKER_UNREACHABLE)
|
||||
|
||||
|
||||
def availability(mode, coordinator_online, broker_reachable=True, why=None):
|
||||
def availability(mode, coordinator_online, broker_reachable=True, why=None,
|
||||
assume_default=False):
|
||||
"""Whether a teacher may send a call now, and what to say if not.
|
||||
|
||||
Returns (can_send, message). The wording for an off mode is the
|
||||
coordinator's own, so the room says the same thing the sanctuary
|
||||
would.
|
||||
would. A message of None means the question is not answered yet
|
||||
rather than answered no.
|
||||
|
||||
assume_default is the caller saying the retained topics have had
|
||||
their chance. Until then a missing mode is undecided; after it, a
|
||||
missing mode is the coordinator's own default — see below.
|
||||
"""
|
||||
if not broker_reachable:
|
||||
return False, broker_message(why)
|
||||
@@ -304,6 +310,23 @@ def availability(mode, coordinator_online, broker_reachable=True, why=None):
|
||||
return False, WORDING[UNAVAILABLE]
|
||||
|
||||
if mode is None:
|
||||
return False, "Parent Call has not been set up yet"
|
||||
# No retained mode on the broker at all.
|
||||
#
|
||||
# This is not a room that was never set up, and treating it as
|
||||
# one is what every classroom did: the card said "Parent Call has
|
||||
# not been set up yet" on machines whose broker, room and
|
||||
# coordinator were all exactly right. The coordinator publishes
|
||||
# the retained mode only in answer to an operator writing to
|
||||
# …/mode/set, and on this church's broker nobody ever has, so the
|
||||
# topic does not exist. Read from the real broker: …/coordinator
|
||||
# and …/pending are retained, …/mode is absent.
|
||||
#
|
||||
# So a mode nobody has set is the coordinator's own DEFAULT_MODE,
|
||||
# and the sanctuary is the end that knows it. The room offers the
|
||||
# call and lets the coordinator answer; if the true mode is off it
|
||||
# refuses, in its own words, through the status topic. A wrong no
|
||||
# here costs a teacher the call. A wrong yes costs a round trip
|
||||
# and tells the truth at the end of it.
|
||||
return (True, None) if assume_default else (False, None)
|
||||
|
||||
return True, None
|
||||
|
||||
@@ -30,7 +30,7 @@ def notify(title, message, urgency="normal"):
|
||||
subprocess.Popen(
|
||||
[
|
||||
"notify-send",
|
||||
"--app-name=Ekklesia Classroom",
|
||||
"--app-name=DVD",
|
||||
f"--urgency={urgency}",
|
||||
"--",
|
||||
title,
|
||||
|
||||
@@ -208,7 +208,7 @@ if [[ "$dry_run" == true ]]; then
|
||||
fi
|
||||
|
||||
"${runner[@]}" notify-send \
|
||||
--app-name="Ekklesia Classroom" \
|
||||
--app-name="Classroom" \
|
||||
--urgency="$urgency" \
|
||||
-- \
|
||||
"$title" "$body"
|
||||
|
||||
@@ -39,6 +39,37 @@ gi.require_version("Gdk", "3.0")
|
||||
|
||||
from gi.repository import Gdk, GLib, Gtk # noqa: E402
|
||||
|
||||
# Where the card sits on the screen.
|
||||
#
|
||||
# A Wayland client cannot place its own window, and the card used to be
|
||||
# centred because KWin centres everything. On a classroom television that
|
||||
# puts it in the middle of whatever the room is watching, a long way from
|
||||
# the button that opened it. Layer shell is the compositor's own answer:
|
||||
# the surface is anchored to an edge and the compositor does the
|
||||
# arithmetic, so bottom-left is bottom-left at 1080p and at 4K, at 100%
|
||||
# and at 200%, with no coordinates written down anywhere.
|
||||
#
|
||||
# The classroom already draws the disc player's cover and control bar
|
||||
# this way under KWin, so the mechanism is one this room has run for
|
||||
# months rather than a new dependency.
|
||||
#
|
||||
# Optional on purpose. If the library is missing the card still opens,
|
||||
# centred, exactly as it did before: a teacher can always reach it.
|
||||
try:
|
||||
gi.require_version("GtkLayerShell", "0.1")
|
||||
from gi.repository import GtkLayerShell # noqa: E402
|
||||
except (ImportError, ValueError):
|
||||
GtkLayerShell = None
|
||||
|
||||
# Logical pixels, so they mean the same thing at every scale.
|
||||
#
|
||||
# The bar is 76 logical pixels tall and reserves that space, so the
|
||||
# compositor already keeps a bottom-anchored surface clear of it; this
|
||||
# gap is the breathing room above it. The left margin lines the card up
|
||||
# near the launcher that opened it rather than hard against the edge.
|
||||
CARD_GAP_BOTTOM = 16
|
||||
CARD_GAP_LEFT = 16
|
||||
|
||||
sys.path.insert(0, "/usr/local/lib/ekklesia-classroom")
|
||||
sys.path.insert(
|
||||
0,
|
||||
@@ -395,11 +426,7 @@ class ParentCall(Gtk.Window):
|
||||
self.set_titlebar(Gtk.Box())
|
||||
self.set_resizable(False)
|
||||
self.set_default_size(460, -1)
|
||||
# Deliberately not set_position(): a Wayland client cannot place
|
||||
# itself, and asking for the centre put the card in the top-left
|
||||
# corner instead. KWin centres it, because kwinrc already asks for
|
||||
# every window to be centred — no rule of its own is needed, and
|
||||
# the one that was written for it turned out to be doing nothing.
|
||||
self._anchor_bottom_left()
|
||||
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
# Belt and braces with the window rule: whichever of the two KWin
|
||||
# honours, the card must not end up behind the lesson a teacher
|
||||
@@ -499,6 +526,44 @@ class ParentCall(Gtk.Window):
|
||||
self.get_screen(), css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
def _anchor_bottom_left(self):
|
||||
"""Put the card in the corner its launcher is in.
|
||||
|
||||
Anchored rather than positioned: the compositor is told which two
|
||||
edges to hold the surface against and works the rest out itself,
|
||||
so this is right on a 1080p television at 100% and on a 4K one at
|
||||
200% without either number appearing here.
|
||||
|
||||
Keyboard interactivity is the part that matters and the part that
|
||||
has to be asked for: a teacher types a child's name into this.
|
||||
"""
|
||||
if GtkLayerShell is None:
|
||||
return
|
||||
|
||||
try:
|
||||
GtkLayerShell.init_for_window(self)
|
||||
# TOP, not OVERLAY. The disc player's cover is an overlay and
|
||||
# is meant to be in front of everything including this; a
|
||||
# teacher fetching a parent during a film is not a reason to
|
||||
# put a text box over the film's own controls.
|
||||
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.BOTTOM, True)
|
||||
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, True)
|
||||
GtkLayerShell.set_margin(
|
||||
self, GtkLayerShell.Edge.BOTTOM, CARD_GAP_BOTTOM)
|
||||
GtkLayerShell.set_margin(
|
||||
self, GtkLayerShell.Edge.LEFT, CARD_GAP_LEFT)
|
||||
# Reserve nothing. The card is a moment, not furniture, and a
|
||||
# surface that reserved space would push the lesson around
|
||||
# every time somebody fetched a parent.
|
||||
GtkLayerShell.set_exclusive_zone(self, 0)
|
||||
GtkLayerShell.set_keyboard_interactivity(self, True)
|
||||
except Exception as error: # noqa: BLE001
|
||||
# Any failure here is cosmetic. A centred card a teacher can
|
||||
# type into beats a perfectly placed one that never opened.
|
||||
print(f"classroom-parent-call: layer shell unavailable: {error}",
|
||||
file=sys.stderr)
|
||||
|
||||
def _set_status(self, text, tone=None, detail=None):
|
||||
style = self.status.get_style_context()
|
||||
|
||||
@@ -520,7 +585,8 @@ class ParentCall(Gtk.Window):
|
||||
and self.broker_reachable
|
||||
and contract.clean_child(self.entry.get_text()) is not None
|
||||
and contract.availability(
|
||||
self.mode, self.coordinator_online, self.broker_reachable
|
||||
self.mode, self.coordinator_online, self.broker_reachable,
|
||||
assume_default=self.settled,
|
||||
)[0]
|
||||
)
|
||||
self.primary.set_sensitive(bool(ready))
|
||||
@@ -585,7 +651,8 @@ class ParentCall(Gtk.Window):
|
||||
return
|
||||
|
||||
can, why = contract.availability(
|
||||
self.mode, self.coordinator_online, self.broker_reachable
|
||||
self.mode, self.coordinator_online, self.broker_reachable,
|
||||
assume_default=final,
|
||||
)
|
||||
|
||||
if can:
|
||||
@@ -642,8 +709,11 @@ class ParentCall(Gtk.Window):
|
||||
self._set_status("Enter the child's name", tone="bad")
|
||||
return
|
||||
|
||||
# The teacher is pressing the button, so the retained topics have
|
||||
# long since had their chance.
|
||||
can, why = contract.availability(
|
||||
self.mode, self.coordinator_online, self.broker_reachable
|
||||
self.mode, self.coordinator_online, self.broker_reachable,
|
||||
assume_default=True,
|
||||
)
|
||||
|
||||
if not can:
|
||||
@@ -673,11 +743,16 @@ class ParentCall(Gtk.Window):
|
||||
|
||||
def _retained_deadline(self):
|
||||
# Retained topics arrive at once or not at all. Now that both have
|
||||
# had their chance, a negative is safe to show.
|
||||
# had their chance, a negative is safe to show, and a mode nobody
|
||||
# has ever set is the coordinator's default rather than a room
|
||||
# that was never configured.
|
||||
self._settle(final=True)
|
||||
|
||||
if not self.settled:
|
||||
self._unavailable("Parent Call has not been set up yet")
|
||||
# Only reachable if nothing at all came back from the
|
||||
# sanctuary end, which the wording should say rather than
|
||||
# blaming this room's setup.
|
||||
self._unavailable("No answer from the sanctuary")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ log="$(cat "$NOTIFY_LOG")"
|
||||
check "runtime dir taken from the compositor" "XDG_RUNTIME_DIR=$RUNTIME" "$log"
|
||||
check "bus address taken from the compositor" "DBUS_SESSION_BUS_ADDRESS=unix:path=$RUNTIME/bus" "$log"
|
||||
check "wayland display from the compositor" "WAYLAND_DISPLAY=wayland-7" "$log"
|
||||
check "styling preserved" "--app-name=Ekklesia Classroom" "$log"
|
||||
check "styling preserved" "--app-name=Classroom" "$log"
|
||||
check "default sample title" "DVD Ready" "$log"
|
||||
check "default sample body" "A disc is ready to play." "$log"
|
||||
|
||||
@@ -183,7 +183,7 @@ echo "4. root delivers as the classroom user"
|
||||
FAKE_CURRENT_UID=0
|
||||
out="$(run)"
|
||||
check "runuser targets classroom" "runuser -u classroom --" "$(cat "$RUNUSER_LOG")"
|
||||
check "notification still sent" "--app-name=Ekklesia Classroom" "$(cat "$NOTIFY_LOG")"
|
||||
check "notification still sent" "--app-name=Classroom" "$(cat "$NOTIFY_LOG")"
|
||||
FAKE_CURRENT_UID=4000
|
||||
|
||||
# ------------------------------------ 5. unprivileged admin is told how
|
||||
@@ -276,7 +276,7 @@ export XDG_RUNTIME_DIR="$RUNTIME"
|
||||
|
||||
out="$(run)"
|
||||
check "reports delivery to this session" "Sent normal notification to this session" "$out"
|
||||
check "notification was sent" "--app-name=Ekklesia Classroom" "$(cat "$NOTIFY_LOG")"
|
||||
check "notification was sent" "--app-name=Classroom" "$(cat "$NOTIFY_LOG")"
|
||||
check "inherits the session display" "WAYLAND_DISPLAY=wayland-4" "$(cat "$NOTIFY_LOG")"
|
||||
check_absent "did not go through runuser" "runuser" "$(cat "$RUNUSER_LOG")"
|
||||
|
||||
|
||||
@@ -154,9 +154,31 @@ check("no broker stops it", not can)
|
||||
check("and says the network, not the sanctuary",
|
||||
"network" in why, why)
|
||||
|
||||
# No retained mode on the broker. The coordinator publishes one only when
|
||||
# an operator writes to …/mode/set, and on the church's broker nobody
|
||||
# ever has — so every room was told it had never been set up.
|
||||
can, why = availability(None, True)
|
||||
check("a broker with no retained mode stops it", not can)
|
||||
check("and says it is not set up", "set up" in why, why)
|
||||
check("an undecided mode is not yet a yes", not can)
|
||||
check("and is not a refusal either", why is None, why)
|
||||
|
||||
can, why = availability(None, True, assume_default=True)
|
||||
check("once the retained topics have had their chance, the call goes",
|
||||
can)
|
||||
check("with nothing to say about it", why is None, why)
|
||||
|
||||
can, why = availability(MODE_OFF, True, assume_default=True)
|
||||
check("a mode that really is off still stops it", not can)
|
||||
check("in the sanctuary's own words", why == WORDING[UNAVAILABLE], why)
|
||||
|
||||
can, why = availability(None, False, assume_default=True)
|
||||
check("and a coordinator that is down still stops it", not can)
|
||||
check("naming the sanctuary, not this room", "sanctuary" in why, why)
|
||||
|
||||
can, why = availability(None, True, broker_reachable=False,
|
||||
why="not configured")
|
||||
check("a room with no broker configured is still its own answer", not can)
|
||||
check("and says this room, not the network",
|
||||
"no church network set up" in why, why)
|
||||
|
||||
print()
|
||||
print(f"{passed} passed, {failed} failed")
|
||||
|
||||
@@ -314,6 +314,41 @@ check("and cannot ask to",
|
||||
chrome_policy.get("NotificationsAllowedForUrls") == []
|
||||
and chrome_policy.get("NotificationsBlockedForUrls") == ["*"])
|
||||
|
||||
print()
|
||||
print("7c-i. the room does not introduce itself")
|
||||
# A teacher does not need the name of the software. Every visible name is
|
||||
# the thing it does — Parent Call, DVD, Browser, Classroom — and the
|
||||
# product name stays where it belongs: package names, unit names, paths
|
||||
# and topics, none of which anybody in the room reads.
|
||||
visible = {}
|
||||
|
||||
for entry in sorted((root / "config/plasma").rglob("*.desktop")):
|
||||
for line in entry.read_text().splitlines():
|
||||
if line.split("=", 1)[0] in ("Name", "GenericName", "Comment"):
|
||||
visible[f"{entry.name}:{line}"] = line
|
||||
|
||||
for script in sorted((root / "scripts").iterdir()):
|
||||
if not script.is_file():
|
||||
continue
|
||||
for line in script.read_text().splitlines():
|
||||
if "--app-name" in line:
|
||||
visible[f"{script.name}:{line.strip()}"] = line
|
||||
|
||||
check("there are visible names to check", len(visible) > 10)
|
||||
|
||||
offenders = [where for where, line in visible.items() if "Ekklesia" in line]
|
||||
|
||||
if offenders:
|
||||
for where in offenders:
|
||||
print(f" {where}")
|
||||
|
||||
check(f"none of them says the product name ({len(visible)} checked)",
|
||||
not offenders)
|
||||
|
||||
check("the notification card is from the room",
|
||||
'--app-name="Classroom"'
|
||||
in (root / "scripts/classroom-notify").read_text())
|
||||
|
||||
print()
|
||||
print("7c-ii. the v2 channel is its own")
|
||||
v2 = (root / "update_classroom_v2.sh").read_text()
|
||||
|
||||
@@ -135,6 +135,22 @@ reboot)
|
||||
done
|
||||
docker exec "$NAME" ln -sf /usr/bin/qdbus6 /usr/local/bin/qdbus
|
||||
|
||||
# The fence again. Routes do not survive a container restart, and the
|
||||
# lab reached the church broker through exactly that gap: four rooms
|
||||
# named after container ids on the real broker, from sessions that
|
||||
# had been fenced when they started and were not after a reboot.
|
||||
docker exec "$NAME" sh -c '
|
||||
for net in 10.30.0.0/16 10.40.0.0/16; do
|
||||
ip route replace blackhole "$net" || exit 1
|
||||
done' || echo " WARNING: could not fence the church networks"
|
||||
|
||||
for target in 10.40.0.2/1883 10.30.0.2/1025; do
|
||||
if docker exec "$NAME" timeout 4 bash -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then
|
||||
echo "REFUSING TO CONTINUE: ${target} is reachable from the lab" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# SDDM really does autologin here: it authenticates the classroom
|
||||
# through PAM and launches the Plasma session, and then has no display
|
||||
# hardware to draw it on. That is worth having — it is the real login
|
||||
|
||||
@@ -109,8 +109,11 @@ import pathlib
|
||||
p = pathlib.Path("/etc/ekklesia-classroom/device.conf")
|
||||
t = p.read_text()
|
||||
t = t.replace("MQTT_ENABLED=false", "MQTT_ENABLED=true")
|
||||
if "MQTT_HOST=127.0.0.1" not in t:
|
||||
t = t.replace("MQTT_HOST=", "MQTT_HOST=127.0.0.1", 1)
|
||||
# The whole line, not a prefix. Replacing the prefix left
|
||||
# MQTT_HOST=127.0.0.1broker.example.invalid on any machine whose broker
|
||||
# was already set — which, since the package started shipping a default,
|
||||
# is every machine.
|
||||
t = re.sub(r"^MQTT_HOST=.*$", "MQTT_HOST=127.0.0.1", t, count=1, flags=re.M)
|
||||
if "\nROOM=" not in t:
|
||||
t += "\n# Lab only.\nROOM=nursery\n"
|
||||
p.write_text(t)
|
||||
|
||||