feat: self-host Hobbs Family Organizer (Shield TV dashboard) on hf-pdocker-02:8340

- tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0: release bundle (Next.js app + Shield APK v1.4 + source)
- compose/hf-pdocker-02/hobbs-organizer: override (local-disk secrets, watchtower off), .env (port 8340), Shield provisioning script
- CLAUDE.md: service tree + internal ports
Trusted-LAN mode. Secrets live only on .100 /var/lib/hobbs-organizer/secrets (not in git).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9NnVEico3WvFxLyjy1iW3
This commit is contained in:
2026-11-04 18:27:22 -06:00
parent 122ea392d1
commit e9c7e9df1a
68 changed files with 12831 additions and 0 deletions
@@ -0,0 +1,182 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
SCRIPT_DIRECTORY="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
PROJECT_ROOT="$(cd -- "$SCRIPT_DIRECTORY/.." && pwd -P)"
SECRETS_DIRECTORY="$PROJECT_ROOT/secrets"
CALENDAR_FILE="$SECRETS_DIRECTORY/calendar_ical_urls.json"
TOKEN_FILE="$SECRETS_DIRECTORY/organizer_access_token.txt"
ENVIRONMENT_FILE="$PROJECT_ROOT/.env"
ENVIRONMENT_EXAMPLE="$PROJECT_ROOT/.env.example"
MODE=""
ROTATE_TOKEN=0
usage() {
printf '%s\n' "Usage: $0 [--mode trusted-lan|bearer] [--rotate-token]" >&2
}
while (($#)); do
case "$1" in
--mode)
(($# >= 2)) || { usage; exit 2; }
MODE="$2"
shift 2
;;
--mode=*)
MODE="${1#*=}"
shift
;;
--rotate-token)
ROTATE_TOKEN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage
exit 2
;;
esac
done
if [[ -n "$MODE" && "$MODE" != "trusted-lan" && "$MODE" != "bearer" ]]; then
printf '%s\n' "Access mode must be trusted-lan or bearer." >&2
exit 2
fi
if [[ ! -f "$ENVIRONMENT_EXAMPLE" ]]; then
printf '%s\n' "Missing .env.example. Run this script from an intact release bundle." >&2
exit 1
fi
mkdir -p -- "$SECRETS_DIRECTORY"
chmod 700 -- "$SECRETS_DIRECTORY"
if [[ ! -e "$ENVIRONMENT_FILE" ]]; then
cp -- "$ENVIRONMENT_EXAMPLE" "$ENVIRONMENT_FILE"
chmod 600 -- "$ENVIRONMENT_FILE"
printf '%s\n' "Created .env from .env.example."
else
printf '%s\n' "Preserved the existing .env file."
fi
replace_calendars=1
if [[ -f "$CALENDAR_FILE" ]]; then
read -r -p "A calendar secret already exists. Replace it? [y/N] " calendar_answer
case "${calendar_answer,,}" in
y|yes) replace_calendars=1 ;;
*) replace_calendars=0 ;;
esac
fi
if ((replace_calendars)); then
calendar_pattern='^https://calendar[.]google[.]com/calendar/ical/[A-Za-z0-9._~%+@-]+/(private-[A-Za-z0-9._~%+@-]+|public)/basic[.]ics$'
calendar_urls=()
printf '%s\n' "Enter private Google Calendar iCal URLs. Input is hidden; submit a blank entry when finished."
while true; do
printf 'Calendar URL #%d: ' "$(( ${#calendar_urls[@]} + 1 ))" >&2
IFS= read -r -s calendar_url
printf '\n' >&2
[[ -n "$calendar_url" ]] || break
if [[ ! "$calendar_url" =~ $calendar_pattern ]]; then
printf '%s\n' "That entry is not a supported private Google Calendar basic.ics HTTPS URL; it was not saved." >&2
unset calendar_url
continue
fi
calendar_urls+=("$calendar_url")
unset calendar_url
done
if ((${#calendar_urls[@]} == 0)); then
printf '%s\n' "At least one calendar URL is required when creating the calendar secret." >&2
exit 1
fi
calendar_temporary="$(mktemp "$SECRETS_DIRECTORY/.calendar-XXXXXXXX")"
cleanup_calendar() {
[[ -n "${calendar_temporary:-}" && -e "$calendar_temporary" ]] && rm -f -- "$calendar_temporary"
}
trap cleanup_calendar EXIT HUP INT TERM
chmod 600 -- "$calendar_temporary"
{
printf '['
for calendar_index in "${!calendar_urls[@]}"; do
((calendar_index == 0)) || printf ','
printf '"%s"' "${calendar_urls[$calendar_index]}"
done
printf ']\n'
} >"$calendar_temporary"
mv -f -- "$calendar_temporary" "$CALENDAR_FILE"
calendar_temporary=""
trap - EXIT HUP INT TERM
chmod 600 -- "$CALENDAR_FILE"
printf 'Saved %d calendar source(s) without displaying their URLs.\n' "${#calendar_urls[@]}"
unset calendar_urls
else
chmod 600 -- "$CALENDAR_FILE"
printf '%s\n' "Preserved the existing calendar secret."
fi
has_existing_token=0
[[ -s "$TOKEN_FILE" ]] && has_existing_token=1
if [[ -z "$MODE" ]]; then
default_mode="trusted-lan"
((has_existing_token)) && default_mode="bearer"
read -r -p "Access mode [trusted-lan/bearer] (default: $default_mode) " selected_mode
MODE="${selected_mode:-$default_mode}"
if [[ "$MODE" != "trusted-lan" && "$MODE" != "bearer" ]]; then
printf '%s\n' "Access mode must be trusted-lan or bearer." >&2
exit 2
fi
fi
if [[ "$MODE" == "trusted-lan" ]]; then
if ((ROTATE_TOKEN)); then
printf '%s\n' "--rotate-token can only be used with bearer mode." >&2
exit 2
fi
if ((has_existing_token)); then
read -r -p "This will erase the current access token and disable bearer authentication. Continue? [y/N] " erase_answer
case "${erase_answer,,}" in
y|yes) ;;
*) printf '%s\n' "Configuration cancelled before changing the access mode." >&2; exit 1 ;;
esac
fi
token_temporary="$(mktemp "$SECRETS_DIRECTORY/.token-XXXXXXXX")"
chmod 600 -- "$token_temporary"
: >"$token_temporary"
mv -f -- "$token_temporary" "$TOKEN_FILE"
chmod 600 -- "$TOKEN_FILE"
printf '%s\n' "Configured trusted-LAN mode with an empty token file."
elif ((has_existing_token)) && ((ROTATE_TOKEN == 0)); then
chmod 600 -- "$TOKEN_FILE"
printf '%s\n' "Preserved the existing bearer token. Use --rotate-token to replace it."
else
token_temporary="$(mktemp "$SECRETS_DIRECTORY/.token-XXXXXXXX")"
cleanup_token() {
[[ -n "${token_temporary:-}" && -e "$token_temporary" ]] && rm -f -- "$token_temporary"
}
trap cleanup_token EXIT HUP INT TERM
chmod 600 -- "$token_temporary"
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 48 2>/dev/null | tr -d '\r\n=' | tr '+/' '-_' >"$token_temporary"
elif command -v base64 >/dev/null 2>&1 && [[ -r /dev/urandom ]]; then
dd if=/dev/urandom bs=48 count=1 2>/dev/null | base64 | tr -d '\r\n=' | tr '+/' '-_' >"$token_temporary"
else
printf '%s\n' "Bearer mode requires openssl, or base64 with /dev/urandom." >&2
exit 1
fi
[[ -s "$token_temporary" ]] || { printf '%s\n' "Secure token generation failed." >&2; exit 1; }
mv -f -- "$token_temporary" "$TOKEN_FILE"
token_temporary=""
trap - EXIT HUP INT TERM
chmod 600 -- "$TOKEN_FILE"
printf '%s\n' "Generated and stored a new bearer token. The token was not displayed."
fi
printf '%s\n' "Configuration is ready. Review .env, then run: docker compose up -d --build"