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]>
164 lines
5.7 KiB
Bash
Executable File
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
|