macOS menu-bar launcher: run ProdDash, and own its permissions
A native Swift app (launcher/macos, ~2 MB, no dependencies) that starts,
stops and watches over the server from the menu bar — and holds the macOS
permissions its modules need.
The permissions are the point as much as the buttons are. macOS grants
privacy permissions to the application that owns a process, and a child
inherits its parent's identity, so with the server started from here node —
and the Timers module's ltc-capture — belong to this app bundle and the
microphone and local-network prompts are asked, and remembered, as ProdDash.
Started over SSH or from a launchd job there is nobody to ask: the permission
is silently absent rather than refused, which is the "endless zeroes" failure
ltc-listener.js warns about.
The shell was already built for a supervisor: with PRODDASH_LAUNCHER=1 it
exits 75 instead of respawning itself when the admin page applies an update,
which the launcher reads as a restart request rather than a fault. Crashes
get a backed-off restart (1s, 2s, 5s, 10s) and four in two minutes stops the
cycle. The server is spawned into its own process group, so stopping it also
stops anything it started — an orphaned ltc-capture would otherwise hold an
audio device indefinitely — and signals, a logout and Quit all stop it first.
- Menu bar: ProdDash's own 2x2 tile mark, its accent tile lit green while
serving, amber mid-move, red when something needs a person. Start, stop,
restart, open the dashboard or admin, open at login, quit.
- Window: status and both addresses, plus Log (also written to
~/Library/Logs/ProdDash, kept a week), Settings, and Permissions.
- The port is written to the data directory's proddash.json — ProdDash's own
file — so the port set here holds however it is next started. Something
already on the port is reported, and offered as a takeover when it answers
like a ProdDash started another way.
- Modules declare what they need in their manifests ("permissions": [...],
documented in docs/MODULE-GUIDE.md and ignored by the shell); the launcher
lists exactly that, with each module's reason, asks for the microphone,
and — since macOS reports nothing about local network access — offers a
check that browses for services and tries every configured upstream.
Timers, ProdCom and ProPresenter modules declare their permissions (patch
bumps). Build with launcher/macos/build.sh; output is gitignored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
abc70aa53d
commit
1dbd55aa24
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/bin/zsh
|
||||
# Build ProdDash.app — the menu-bar launcher.
|
||||
#
|
||||
# ./build.sh build into launcher/macos/build/ProdDash.app
|
||||
# ./build.sh --install …and copy it to /Applications
|
||||
# ./build.sh --run …and launch it when it's built
|
||||
# ./build.sh --arch native build for this Mac only (faster; default is universal)
|
||||
#
|
||||
# Needs the Xcode command line tools (swiftc). No other dependencies — the app
|
||||
# is Swift and system frameworks, nothing to install and nothing to vendor.
|
||||
set -e -u
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
APP_NAME="ProdDash"
|
||||
BUNDLE_ID="org.waterschurch.proddash"
|
||||
OUT_DIR="build"
|
||||
APP="$OUT_DIR/$APP_NAME.app"
|
||||
ARCHS=(arm64 x86_64)
|
||||
INSTALL=0
|
||||
RUN=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--install) INSTALL=1 ;;
|
||||
--run) RUN=1 ;;
|
||||
--arch) shift; [[ "${1:-}" == "native" ]] && ARCHS=($(uname -m)) || ARCHS=("${1:-arm64}") ;;
|
||||
-h|--help) sed -n '2,10p' "$0"; exit 0 ;;
|
||||
*) echo "build.sh: unknown option $1" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if ! command -v swiftc >/dev/null; then
|
||||
echo "build.sh: swiftc not found — install the Xcode command line tools:" >&2
|
||||
echo " xcode-select --install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' Resources/Info.plist)
|
||||
echo "ProdDash Launcher $VERSION — building for ${ARCHS[*]}"
|
||||
|
||||
rm -rf "$OUT_DIR"
|
||||
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$OUT_DIR/obj"
|
||||
|
||||
# One binary per architecture, then lipo them together, so the same .app runs
|
||||
# on the booth's Apple Silicon and on any Intel Mac still in service.
|
||||
SLICES=()
|
||||
for arch in "${ARCHS[@]}"; do
|
||||
echo " compiling $arch"
|
||||
swiftc -O -whole-module-optimization \
|
||||
-target "$arch-apple-macos13.0" \
|
||||
-module-name ProdDashLauncher \
|
||||
-o "$OUT_DIR/obj/$APP_NAME-$arch" \
|
||||
Sources/*.swift
|
||||
SLICES+=("$OUT_DIR/obj/$APP_NAME-$arch")
|
||||
done
|
||||
|
||||
if [[ ${#SLICES[@]} -gt 1 ]]; then
|
||||
lipo -create "${SLICES[@]}" -output "$APP/Contents/MacOS/$APP_NAME"
|
||||
else
|
||||
cp "${SLICES[1]}" "$APP/Contents/MacOS/$APP_NAME"
|
||||
fi
|
||||
chmod +x "$APP/Contents/MacOS/$APP_NAME"
|
||||
|
||||
cp Resources/Info.plist "$APP/Contents/Info.plist"
|
||||
printf 'APPL????' > "$APP/Contents/PkgInfo"
|
||||
|
||||
# App icon: ProdDash's own, straight from the web app's icons.
|
||||
SOURCE_ICON="../../public/icons/icon-512.png"
|
||||
if [[ -f "$SOURCE_ICON" ]]; then
|
||||
echo " building the icon"
|
||||
ICONSET="$OUT_DIR/AppIcon.iconset"
|
||||
mkdir -p "$ICONSET"
|
||||
for size in 16 32 64 128 256 512 1024; do
|
||||
sips -z $size $size "$SOURCE_ICON" --out "$ICONSET/icon_${size}x${size}.png" >/dev/null 2>&1
|
||||
done
|
||||
# iconutil wants Apple's names, including the @2x pairs.
|
||||
mv "$ICONSET/icon_32x32.png" "$ICONSET/icon_16x16@2x.png"
|
||||
cp "$ICONSET/icon_64x64.png" "$ICONSET/icon_32x32@2x.png"
|
||||
mv "$ICONSET/icon_64x64.png" "$ICONSET/icon_32x32.png"
|
||||
cp "$ICONSET/icon_256x256.png" "$ICONSET/icon_128x128@2x.png"
|
||||
cp "$ICONSET/icon_512x512.png" "$ICONSET/icon_256x256@2x.png"
|
||||
mv "$ICONSET/icon_1024x1024.png" "$ICONSET/icon_512x512@2x.png"
|
||||
iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/AppIcon.icns"
|
||||
rm -rf "$ICONSET"
|
||||
else
|
||||
echo " (no $SOURCE_ICON — the app will use the generic icon)"
|
||||
fi
|
||||
|
||||
rm -rf "$OUT_DIR/obj"
|
||||
|
||||
# Ad-hoc signature: enough for macOS to run it and to hang privacy permissions
|
||||
# off it. Note that the signature changes on every rebuild, so macOS may treat
|
||||
# a rebuilt app as a new one and ask for the microphone again.
|
||||
codesign --force --sign - --identifier "$BUNDLE_ID" "$APP" >/dev/null 2>&1 \
|
||||
&& echo " signed (ad-hoc)" \
|
||||
|| echo " warning: could not sign the app — permissions may not stick"
|
||||
|
||||
echo "built $APP"
|
||||
|
||||
if [[ $INSTALL -eq 1 ]]; then
|
||||
DEST="/Applications/$APP_NAME.app"
|
||||
# A running copy holds its own bundle open; stop it before replacing it.
|
||||
osascript -e "tell application \"$APP_NAME\" to quit" >/dev/null 2>&1 || true
|
||||
sleep 1
|
||||
rm -rf "$DEST"
|
||||
cp -R "$APP" "$DEST"
|
||||
echo "installed $DEST"
|
||||
APP="$DEST"
|
||||
fi
|
||||
|
||||
if [[ $RUN -eq 1 ]]; then
|
||||
open "$APP"
|
||||
fi
|
||||
Reference in New Issue
Block a user