diff --git a/scripts/deploy/hardware.sh b/scripts/deploy/hardware.sh index f343a79..71dfc70 100755 --- a/scripts/deploy/hardware.sh +++ b/scripts/deploy/hardware.sh @@ -62,6 +62,18 @@ ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \ && rm -rf \"${REMOTE_TMP}\" '" 2>&1 | tail -3 +# Patch bbctrl Web.py so font files get the correct MIME type. The +# Pi ships Python 3.5, whose `mimetypes` module doesn't know about +# woff/woff2/ttf, so Tornado serves them as application/octet-stream +# which Chromium 72 (the Pi's onboard browser) refuses to use as a +# web font, leading to all FontAwesome icons rendering as empty +# boxes in the kiosk UI. The patch is idempotent. +echo "Patching bbctrl font MIME types (idempotent)..." +scp -o ConnectTimeout=5 "$SCRIPT_DIR/scripts/deploy/patch_font_mime.py" \ + "${REMOTE_USER}@${HOST}:/tmp/patch_font_mime.py" >/dev/null +ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \ + "echo '${PASSWORD}' | sudo -S python3 /tmp/patch_font_mime.py" 2>&1 | tail -3 + echo "Restarting bbctrl service..." ssh -o ConnectTimeout=5 "${REMOTE_USER}@${HOST}" \ "echo '${PASSWORD}' | sudo -S systemctl restart bbctrl" 2>&1 | tail -3 diff --git a/scripts/deploy/patch_font_mime.py b/scripts/deploy/patch_font_mime.py new file mode 100644 index 0000000..145a6d6 --- /dev/null +++ b/scripts/deploy/patch_font_mime.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +Patch bbctrl Web.py so font files get the correct MIME type. + +Background +---------- +The Onefinity controller (Pi 3B running Raspbian stretch) ships Python +3.5, whose ``mimetypes`` module does not recognize ``.woff``, ``.woff2`` +or ``.ttf``. Tornado's ``StaticFileHandler`` therefore falls back to +``application/octet-stream`` for those, and Chromium 72 (the Pi's +onboard kiosk browser) refuses to use such payloads as web fonts. The +result is that every FontAwesome icon renders as an empty box on the +kiosk display. + +This patch monkey-patches ``StaticFileHandler.get_content_type`` to +emit the right MIME types. It is idempotent: running it twice is a +no-op. Run with ``sudo`` so it can rewrite the egg's Web.py. + +Used by: + scripts/deploy/hardware.sh +""" + +from __future__ import print_function +import os +import sys + + +def find_web_py(): + """Return the absolute path to the bbctrl Web.py shipped in the egg.""" + base = "/usr/local/lib" + for entry in os.listdir(base): + if not entry.startswith("python"): + continue + candidate_dir = os.path.join(base, entry, "dist-packages") + if not os.path.isdir(candidate_dir): + continue + for sub in os.listdir(candidate_dir): + if sub.startswith("bbctrl-") and sub.endswith(".egg"): + p = os.path.join(candidate_dir, sub, "bbctrl", "Web.py") + if os.path.isfile(p): + return p + return None + + +OLD_BLOCK = ( + "class StaticFileHandler(tornado.web.StaticFileHandler):\n" + " def set_extra_headers(self, path):\n" + " self.set_header('Cache-Control',\n" + " 'no-store, no-cache, must-revalidate, max-age=0')" +) + +NEW_BLOCK = ( + "class StaticFileHandler(tornado.web.StaticFileHandler):\n" + " # FONT_MIME_FIX: Python 3.5's mimetypes module does not know\n" + " # woff/woff2/ttf, so Tornado serves them as application/octet-\n" + " # stream which Chromium 72 (the Pi's onboard kiosk browser)\n" + " # refuses to use as web fonts. Set explicit types so the FA6\n" + " # icon set actually renders on the kiosk display.\n" + " def get_content_type(self):\n" + " path = self.absolute_path or ''\n" + " if path.endswith('.woff2'): return 'font/woff2'\n" + " if path.endswith('.woff'): return 'font/woff'\n" + " if path.endswith('.ttf'): return 'font/ttf'\n" + " if path.endswith('.otf'): return 'font/otf'\n" + " if path.endswith('.eot'): return 'application/vnd.ms-fontobject'\n" + " return super().get_content_type()\n" + "\n" + " def set_extra_headers(self, path):\n" + " self.set_header('Cache-Control',\n" + " 'no-store, no-cache, must-revalidate, max-age=0')" +) + + +def main(): + target = find_web_py() + if target is None: + print("ERROR: could not locate bbctrl Web.py under /usr/local/lib", + file=sys.stderr) + return 1 + + with open(target) as f: + src = f.read() + + if "FONT_MIME_FIX" in src: + print("font mime: already patched ({})".format(target)) + return 0 + + if OLD_BLOCK not in src: + print("font mime: expected block not found in {} -- skipping".format(target), + file=sys.stderr) + # Don't fail the deploy; just log and continue. + return 0 + + new_src = src.replace(OLD_BLOCK, NEW_BLOCK, 1) + with open(target, "w") as f: + f.write(new_src) + print("font mime: patched {}".format(target)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/stylus/style.styl b/src/stylus/style.styl index 75206c1..0196f16 100644 --- a/src/stylus/style.styl +++ b/src/stylus/style.styl @@ -2570,3 +2570,51 @@ html.kiosk-mode .control-page .jog-head > * + * margin-left 8px + + // Settings rail must be scrollable in kiosk mode \u2014 the 14+ + // item list overflows the 768px viewport at default heights. + .settings-shell + grid-template-columns 220px 1fr + gap 10px + + .settings-rail + position static + align-self stretch + max-height 100% + overflow-y auto + + .settings-rail .set-item + height 36px + font-size 0.85rem + padding 0 10px + + .fa + width 14px + font-size 0.9rem + + .settings-rail .set-section + margin 6px 4px 2px + font-size 0.62rem + + .settings-rail .set-rail-foot + margin-top 4px + padding-top 6px + + .sp-shutdown, .sp-save + height 32px + font-size 0.85rem + + +// Program tab flex-gap fallbacks for Chromium 72. +// Action bar (RUN/STOP/UPLOAD/.../DELETE) and the action buttons +// themselves (icon stacked over label). +.action-bar > * + * + margin-left 12px +.action-btn > * + * + margin-top 4px + +// File bar (Create Folder / folder select / file select / sort). +.file-bar > * + * + margin-left 10px +.file-btn > * + * + margin-left 0.4rem