Files
Classroom_Desktop/tools/lab/parent-call-rig
T
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

164 lines
5.7 KiB
Bash
Executable File

#!/bin/bash
# A Parent Call sanctuary that cannot reach the sanctuary.
#
# The real coordinator, from the control computer, running in the lab
# against a broker on loopback and a ProPresenter that is a mock. The
# church's own networks are rejected from inside the container first, so
# a wrong host in a config file cannot put a child's name on the real
# screen. Coordinator development has done that once already.
#
# tools/lab/parent-call-rig up [MODE] # off | approval | auto
# tools/lab/parent-call-rig mode MODE
# tools/lab/parent-call-rig approve|reject
# tools/lab/parent-call-rig watch
# tools/lab/parent-call-rig triggers
# tools/lab/parent-call-rig down
set -uo pipefail
NAME="${LAB_NAME:-classroom-v2-lab}"
SRC="${PARENT_CALL_SRC:-classroom-control:/home/media/docker/parent-call}"
ROOT=classroom/parent-call
inlab() { docker exec "$NAME" "$@"; }
guard() {
# Container-local. This netns belongs to the container alone, so
# nothing on the host or the church network is changed by it.
# Blackhole routes, not iptables: the lab image has no iptables at
# all, so these two lines had been failing silently since they were
# written. `tools/lab/lab up` now does the same thing at start-up, so
# this is a second statement of it rather than the only one.
inlab sh -c '
for net in 10.30.0.0/16 10.40.0.0/16; do
ip route replace blackhole "$net" || exit 1
done'
# bash, not sh. /dev/tcp is a bash feature and this probe reported
# every target unreachable under dash, whatever the network was doing.
for target in 10.40.0.2/1883 10.30.0.2/1025; do
if inlab timeout 4 bash -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then
echo "REFUSING TO CONTINUE: ${target} is still reachable" >&2
exit 1
fi
done
echo " church networks unreachable from the lab"
}
case "${1:-up}" in
up)
mode="${2:-approval}"
inlab sh -c 'apt-get install -y -qq --no-install-recommends \
mosquitto mosquitto-clients python3-paho-mqtt iptables >/dev/null 2>&1'
guard
inlab sh -c 'mkdir -p /opt/pc-test'
ssh -o BatchMode=yes "${SRC%%:*}" "cd ${SRC#*:} && tar cf - src" \
| docker exec -i "$NAME" sh -c 'cd /opt/pc-test && tar xf -'
echo " coordinator source copied from the control computer"
docker exec -i "$NAME" sh -c 'cat > /opt/pc-test/mosquitto.conf' <<CONF
# Loopback only. Nothing published here can leave the container.
listener 1883 127.0.0.1
allow_anonymous true
persistence false
CONF
docker exec -i "$NAME" sh -c 'cat > /opt/pc-test/coordinator.env' <<ENV
MQTT_HOST=127.0.0.1
MQTT_PORT=1883
MQTT_CLIENT_ID=parent-call-lab
TOPIC_ROOT=$ROOT
PROPRESENTER_BASE_URL=http://127.0.0.1:1025
PROPRESENTER_MESSAGE='Parent Caller'
PROPRESENTER_CHILD_TOKEN=Name
PROPRESENTER_ROOM_TOKEN=Room
PROPRESENTER_TIMEOUT=4
PROPRESENTER_CLEAR_AFTER=20
ROOM_NAMES='nursery=Nursery,k2=K-2,older=Older Kids'
REQUEST_TTL=180
MAX_CHILD_LENGTH=40
MAX_ROOM_LENGTH=32
MAX_CLOCK_SKEW=120
DEFAULT_MODE=$mode
HEALTH_PORT=8099
ENV
docker exec -i "$NAME" sh -c 'cat > /opt/propresenter-mock.py' \
< "$(dirname "$0")/propresenter-mock.py"
# Debian's own mosquitto unit would listen where the lab does not want
# it to. The rig runs its own, on loopback, and nothing else.
inlab sh -c 'systemctl disable --now mosquitto >/dev/null 2>&1;
pkill -f propresenter-mock; pkill -f parent_call.main; pkill mosquitto;
fuser -k 1025/tcp 1883/tcp 2>/dev/null; true'
sleep 3
inlab sh -c 'setsid mosquitto -c /opt/pc-test/mosquitto.conf > /tmp/mosquitto.log 2>&1 &'
inlab sh -c 'setsid python3 /opt/propresenter-mock.py 1025 > /tmp/ppmock.log 2>&1 &'
sleep 2
inlab sh -c 'cd /opt/pc-test && set -a && . ./coordinator.env && set +a &&
setsid env PYTHONPATH=/opt/pc-test/src python3 -m parent_call.main \
> /tmp/coordinator.log 2>&1 &'
sleep 4
# The room points at the loopback broker, never at the church's.
# docker exec needs -i or the heredoc reaches nothing at all, which is
# how this silently left the room pointing nowhere.
docker exec -i "$NAME" python3 - <<'PY'
import pathlib
p = pathlib.Path("/etc/ekklesia-classroom/device.conf")
t = p.read_text()
t = t.replace("MQTT_ENABLED=false", "MQTT_ENABLED=true")
# 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)
PY
"$0" mode "$mode"
inlab tail -3 /tmp/coordinator.log
echo " rig up in mode $mode"
;;
mode)
inlab mosquitto_pub -h 127.0.0.1 -t "$ROOT/mode/set" -m "${2:?mode}"
sleep 1
echo -n " mode is now: "
inlab timeout 3 mosquitto_sub -h 127.0.0.1 -t "$ROOT/mode" -C 1 || true
;;
approve|reject)
id=$(inlab timeout 3 mosquitto_sub -h 127.0.0.1 -t "$ROOT/pending" -C 1 |
python3 -c 'import json,sys; print(json.load(sys.stdin).get("id") or "")')
[ -n "$id" ] || { echo " nothing is waiting"; exit 0; }
inlab mosquitto_pub -h 127.0.0.1 -t "$ROOT/$1" -m "{\"id\": \"$id\"}"
echo " $1ed $id"
;;
watch)
inlab mosquitto_sub -h 127.0.0.1 -t "$ROOT/#" -v
;;
triggers)
inlab sh -c 'cat /tmp/propresenter-mock.log 2>/dev/null || echo "(none)"'
;;
log)
inlab tail -n "${2:-25}" /tmp/coordinator.log
;;
down)
inlab sh -c 'pkill -f parent_call.main; pkill -f propresenter-mock; pkill mosquitto; true'
echo " rig down"
;;
*)
sed -n '2,14p' "$0"
exit 1
;;
esac