0.6.6: Digest format final: plain reflowing rows instead of <pre> columns

Iterating on the 0.6.5 HTML digest on a real phone: <pre> column layout
forces fixed-width lines that wrap into unreadable fragments ("54 tracks
in / M3U"), and one <pre> per section rendered as a stack of copy-button
widgets. Every row is now plain proportional text -- colored dot, bold
label, "-- value" -- which reflows cleanly at any screen width; breakdowns
(added-today per playlist, library by format) become bullet lists. Header
banner (brand line, dot tally, all-clear/N-issues blockquote) unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
andrew
2026-07-15 13:44:26 -06:00
parent be33664be2
commit 900bbff8aa
2 changed files with 71 additions and 59 deletions
+3 -3
View File
@@ -73,10 +73,10 @@ Optional, add these later if you want them:
Pull the prebuilt image onto your Docker host:
```bash
docker pull git.kretzer.club/andrew/alembic:0.6.5
docker pull git.kretzer.club/andrew/alembic:0.6.6
```
That is the whole install. You do not need to download the source or build anything. The `0.6.5` is the version; you can pin to it so nothing changes under you, or use `latest` to always get the newest.
That is the whole install. You do not need to download the source or build anything. The `0.6.6` is the version; you can pin to it so nothing changes under you, or use `latest` to always get the newest.
(If you would rather build it yourself from source, you can, but you do not need to.)
@@ -136,7 +136,7 @@ Create a file called `docker-compose.yml` on your server (put it wherever you ke
```yaml
services:
alembic:
image: git.kretzer.club/andrew/alembic:0.6.5
image: git.kretzer.club/andrew/alembic:0.6.6
container_name: alembic
ports:
- "8420:8420"
+68 -56
View File
@@ -4,16 +4,18 @@
# Writes a one-screen summary to ${ALEMBIC_CONFIG_DIR:-/config}/logs/STATUS.log (overwritten daily)
# and ships it to Telegram via notify-telegram.sh.
#
# Formatting note: the digest now uses Telegram HTML entities (<b>, <i>, <pre>,
# Formatting note: the digest uses Telegram HTML entities (<b>, <i>, <code>,
# <blockquote>) — the brand's chrome/dot-badge language translated into what
# Telegram can actually render (no color, no custom font, so status "dots"
# become colored circle emoji and section cards become bold header + <pre>
# block, matching the web UI's h2 + mono table pattern). This REQUIRES
# notify-telegram.sh to be called with --html (parse_mode=HTML) — sent as
# plain text the tags would show up literally.
# All free-text going through mark_ok/mark_warn/mark_skip/mark_info/section is
# escaped (esc()) for &, <, > so a stray angle bracket in a log line can't
# break the HTML parse and swallow the whole message.
# become colored circle emoji). Deliberately NO <pre>/monospace column layout:
# a code block forces fixed-width columns that wrap into unreadable garbage on
# a phone ("54 tracks in / M3U"). Instead every row is plain reflowing text —
# a bold label + " — value" — so it wraps cleanly at any screen width. This
# REQUIRES notify-telegram.sh to be called with --html (parse_mode=HTML) —
# sent as plain text the tags would show up literally. All free-text going
# through mark_ok/mark_warn/mark_skip/section is escaped (esc()) for &, <, >
# so a stray angle bracket in a log line can't break the HTML parse and
# swallow the whole message.
#
# Reports on:
# - Sibling service reachability (slskd, navidrome) via HTTP, not docker ps —
@@ -57,7 +59,6 @@ ND_PASS="${ND_PASS:-}"
OK=0
WARN=0
SKIP=0
PRE_OPEN=0
# Escapes &, <, > so free text (log snippets, exception messages, filenames)
# can't be mistaken for an HTML entity by Telegram's parser.
@@ -69,20 +70,28 @@ esc() {
printf '%s' "$s"
}
# Status lines double as the brand's badge-dot language (success/warning/muted)
# translated into Telegram's only color channel: emoji.
mark_ok() { OK=$((OK+1)); printf " 🟢 %s\n" "$(esc "$*")"; }
mark_warn() { WARN=$((WARN+1)); printf " 🟠 %s\n" "$(esc "$*")"; }
mark_skip() { SKIP=$((SKIP+1)); printf " ⚪ %s\n" "$(esc "$*")"; }
mark_info() { printf " %s\n" "$(esc "$*")"; }
# One status row: a colored dot, a bold label, and (optionally) " — value".
# Plain proportional text — NO padding, NO monospace — so it reflows on mobile
# instead of wrapping mid-column. The dot is the brand's badge-state color,
# emoji being Telegram's only color channel.
_row() {
local dot=$1 label=$2 detail=${3:-}
if [[ -n "$detail" ]]; then
printf "%s <b>%s</b> — %s\n" "$dot" "$(esc "$label")" "$(esc "$detail")"
else
printf "%s <b>%s</b>\n" "$dot" "$(esc "$label")"
fi
}
mark_ok() { OK=$((OK+1)); _row "🟢" "$@"; }
mark_warn() { WARN=$((WARN+1)); _row "🟠" "$@"; }
mark_skip() { SKIP=$((SKIP+1)); _row "⚪" "$@"; }
# Free-text info line (no dot, no counter) — for sub-breakdowns.
mark_info() { printf " %s\n" "$(esc "$*")"; }
# Bold section header (proportional text, reads like the web app's <h2>),
# followed by a <pre> block so the printf-padded columns below it actually
# line up in a fixed-width font. Closes the previous section's <pre> first.
# Bold section header (reads like the web app's <h2>). No <pre> — the rows
# under it are plain reflowing text.
section() {
if [[ "$PRE_OPEN" -eq 1 ]]; then printf "</pre>\n"; fi
printf "\n<b>▎ %s</b>\n<pre>" "$(esc "$*")"
PRE_OPEN=1
printf "\n<b>▎ %s</b>\n" "$(esc "$*")"
}
# syslog / `logger` is not present in the container image. Only call it if it
@@ -114,7 +123,7 @@ playlist_status() {
m3u=$(awk 'match($0, /updated with [0-9]+ tracks/) {
n=substr($0, RSTART, RLENGTH); gsub(/[^0-9]/, "", n); last=n
} END { print last }' "$log")
echo "OK|${m3u:-0} tracks in M3U"
echo "OK|${m3u:-0} tracks"
elif grep -q "=== Starting playlist run" "$log" 2>/dev/null; then
echo "WARN|started but never finished"
else
@@ -174,12 +183,12 @@ dedup_today_status() {
check_http() {
local name="$1" url="$2"
if curl -s -o /dev/null --connect-timeout 3 --max-time 6 "$url"; then
mark_ok "$(printf '%-11s reachable' "$name")"
mark_ok "$name" "reachable"
else
mark_warn "$(printf '%-11s unreachable at %s' "$name" "$url")"
mark_warn "$name" "unreachable at $url"
fi
}
check_http slskd "http://gluetun:5030/"
check_http slskd "http://gluetun:5030/"
check_http navidrome "http://navidrome:4533/rest/ping.view"
# 2. Today's runs
@@ -220,14 +229,12 @@ dedup_today_status() {
fi
;;
esac
line=$(printf '%-13s %s' "$short" "$detail")
if [[ "$tag" == "OK" ]]; then mark_ok "$line"; else mark_warn "$line"; fi
if [[ "$tag" == "OK" ]]; then mark_ok "$short" "$detail"; else mark_warn "$short" "$detail"; fi
done
if [[ -n "$newest_dedup_today" ]]; then
any_today=1
IFS='|' read -r tag detail < <(dedup_today_status "$newest_dedup_today")
line=$(printf '%-13s %s' "dedup" "$detail")
if [[ "$tag" == "OK" ]]; then mark_ok "$line"; else mark_warn "$line"; fi
if [[ "$tag" == "OK" ]]; then mark_ok "dedup" "$detail"; else mark_warn "dedup" "$detail"; fi
fi
[[ "$any_today" -eq 0 ]] && mark_info "(nothing has run yet today)"
@@ -239,15 +246,15 @@ dedup_today_status() {
ADDED_TODAY=$(beet ls -f '$grouping' \
"added:$(date +%Y-%m-%d).." 2>/dev/null || true)
added_total=$(echo -n "$ADDED_TODAY" | grep -c '^' || true)
mark_ok "$(printf '%-13s %d new tracks in beets' 'added today' "$added_total")"
mark_ok "added today" "$added_total new tracks in beets"
if [[ "$added_total" -gt 0 ]]; then
# Per-playlist breakdown. Empty $grouping (bandcamp/manual) shown as "(none)".
# Piped through the same &/</> escaping as mark_*, since this prints
# straight into the <pre> block without going through esc() otherwise.
# Piped through the same &/</> escaping as esc(), since this prints
# straight into the message without going through _row.
echo "$ADDED_TODAY" \
| awk '{ if ($0 == "") print "(none)"; else print }' \
| sort | uniq -c | sort -rn \
| awk '{ g=$2; for(i=3;i<=NF;i++) g=g" "$i; printf " %3d %s\n", $1, g }' \
| awk '{ c=$1; g=$2; for(i=3;i<=NF;i++) g=g" "$i; printf " • %s (%d)\n", g, c }' \
| sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g'
fi
@@ -321,7 +328,7 @@ PYEOF
row=$(grep "^${task}|" <<< "$maint_rows" || true)
IFS='|' read -r _ latest_status latest_age age_s log <<< "$row"
if [[ -z "$row" || "$latest_status" == "none" ]]; then
mark_skip "$(printf '%-22s never run yet' "$task")"
mark_skip "$task" "never run yet"
continue
fi
if [[ "$age_s" -lt 0 ]]; then
@@ -331,7 +338,7 @@ PYEOF
running) note="running now" ;;
*) note="$latest_status" ;;
esac
mark_warn "$(printf '%-22s never succeeded (last attempt %s: %s)' "$task" "$(human_age "$latest_age")" "$note")"
mark_warn "$task" "never succeeded (last attempt $(human_age "$latest_age"): $note)"
continue
fi
age_h=$(human_age "$age_s")
@@ -369,11 +376,12 @@ PYEOF
failed) snip="${snip:+$snip, }latest attempt failed" ;;
skipped_lock) snip="${snip:+$snip, }latest attempt skipped, lock busy" ;;
esac
line=$(printf '%-22s %-9s %s' "$task" "$age_h" "${snip:-}")
detail="$age_h"
[[ -n "$snip" ]] && detail="$age_h · $snip"
if (( age_s > MAINT_LIMIT[$task] )) || [[ "$latest_status" == "failed" ]]; then
mark_warn "$line"
mark_warn "$task" "$detail"
else
mark_ok "$line"
mark_ok "$task" "$detail"
fi
done
@@ -385,13 +393,16 @@ PYEOF
exp=$(awk -F'\t' '$6=="identity" {print $5; exit}' ${ALEMBIC_CONFIG_DIR:-/config}/pipeline/bandcamp/cookies.txt)
if [[ -n "${exp:-}" && "$exp" =~ ^[0-9]+$ && "$exp" -gt 0 ]]; then
days=$(( (exp - NOW_TS) / 86400 ))
line=$(printf '%-22s %d days left' "Bandcamp cookie" "$days")
if (( days < 14 )); then mark_warn "$line — re-export soon"; else mark_ok "$line"; fi
if (( days < 14 )); then
mark_warn "Bandcamp cookie" "$days days left — re-export soon"
else
mark_ok "Bandcamp cookie" "$days days left"
fi
else
mark_warn "Bandcamp cookie could not parse expiry"
mark_warn "Bandcamp cookie" "could not parse expiry"
fi
else
mark_warn "Bandcamp cookie missing (${ALEMBIC_CONFIG_DIR:-/config}/pipeline/bandcamp/cookies.txt)"
mark_warn "Bandcamp cookie" "missing (${ALEMBIC_CONFIG_DIR:-/config}/pipeline/bandcamp/cookies.txt)"
fi
# Qobuz Web Player token (buy-link enrich cascade #2). The token is opaque
@@ -411,12 +422,12 @@ PYEOF
-H "X-App-Id: $q_appid" -H "X-User-Auth-Token: $q_token" -w '%{http_code}' \
"https://www.qobuz.com/api.json/0.2/track/search?query=test&limit=1&app_id=$q_appid" 2>/dev/null)
case "$q_code" in
200) mark_ok "$(printf '%-22s %s' 'Qobuz token' 'valid (buy-link lookup live)')" ;;
401) mark_warn "$(printf '%-22s %s' 'Qobuz token' "EXPIRED — re-export X-User-Auth-Token to ${ALEMBIC_CONFIG_DIR:-/config}/pipeline/qobuz/token")" ;;
*) mark_warn "$(printf '%-22s %s' 'Qobuz token' "check failed (HTTP ${q_code:-none})")" ;;
200) mark_ok "Qobuz token" "valid (buy-link lookup live)" ;;
401) mark_warn "Qobuz token" "EXPIRED — re-export X-User-Auth-Token to ${ALEMBIC_CONFIG_DIR:-/config}/pipeline/qobuz/token" ;;
*) mark_warn "Qobuz token" "check failed (HTTP ${q_code:-none})" ;;
esac
else
mark_warn "$(printf '%-22s %s' 'Qobuz token' "missing (${ALEMBIC_CONFIG_DIR:-/config}/pipeline/qobuz/token)")"
mark_warn "Qobuz token" "missing (${ALEMBIC_CONFIG_DIR:-/config}/pipeline/qobuz/token)"
fi
# VPN egress
@@ -426,17 +437,20 @@ PYEOF
# asking Docker to introspect gluetun's health, and needs no Docker socket.
egress_ip=$(curl -s --connect-timeout 5 --max-time 10 https://ifconfig.me 2>/dev/null)
if [[ -n "$egress_ip" ]]; then
mark_ok "$(printf '%-22s %s' 'VPN egress' "$egress_ip")"
mark_ok "VPN egress" "$egress_ip"
else
mark_warn "$(printf '%-22s %s' 'VPN egress' 'could not determine egress IP — VPN may be down')"
mark_warn "VPN egress" "could not determine egress IP — VPN may be down"
fi
# 5. Storage
section "Storage"
while read -r mp pct used total; do
line=$(printf '%-22s %s used (%s of %s)' "$mp" "$pct" "$used" "$total")
pct_n=${pct%\%}
if (( pct_n > 90 )); then mark_warn "$line"; else mark_ok "$line"; fi
if (( pct_n > 90 )); then
mark_warn "$mp" "$pct used ($used of $total)"
else
mark_ok "$mp" "$pct used ($used of $total)"
fi
done < <(df -h ${MUSIC_DATA_DIR:-/data/music} / 2>/dev/null | awk 'NR>1 && $1!~/tmpfs/ {print $6, $5, $3, $2}')
# 6. Navidrome
@@ -461,15 +475,13 @@ except Exception as e:
print(f\"WARN|unreachable: {e}\")
" 2>/dev/null)
state=${ND_LINE%%|*}; detail=${ND_LINE##*|}
line=$(printf '%-22s %s' "Navidrome" "$detail")
if [[ "$state" == "OK" ]]; then mark_ok "$line"; else mark_warn "$line"; fi
if [[ "$state" == "OK" ]]; then mark_ok "Navidrome" "$detail"; else mark_warn "Navidrome" "$detail"; fi
# 7. Library by format (info-only, no status pill)
# 7. Library by format (info-only, no status dot)
section "Library by format"
beet ls -f '$format' 2>/dev/null | sort | uniq -c | sort -rn \
| awk '{printf " %-6s %d tracks\n", $2, $1}'
[[ "$PRE_OPEN" -eq 1 ]] && printf "</pre>\n"
| awk '{printf " %s — %d tracks\n", $2, $1}' \
| sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g'
} > "$OUT"