Full app audit: 24 fixes across backend, frontend, infra, content, social

Critical fixes:
- Fix hangup-during-respond crash (null caller guard)
- Fix double-click caller race condition
- Stem recorder: non-daemon thread, disk error handling, 30s flush timeout
- Frontend startCall() error handling

High priority:
- Devon: filter tool errors from speech, shorter monitor prompt, 30s interval
- TTS ghost message fix (add to history after TTS, not before)
- Expand banned phrase list (12 new phrases)
- Increase returning callers from 1 to 2 per session
- Platform-tailored social posts with staggered scheduling
- YouTube dynamic tags from episode content
- Social post retry logic (2 attempts, 5s delay)
- Frontend: error handling on all raw fetch calls

Medium:
- stem_recorder null check race (local var capture in audio.py)
- Reactive shape directive expanded
- REACT TO LUKE moved higher in caller prompt
- Devon tenure updated ("few weeks" not "first day")
- D shortcut Escape to unfocus
- Volume slider debounced (150ms)
- Settings modal widened to 550px
- Backup script (daily MariaDB dump + data/ rsync to NAS)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 14:57:50 -06:00
parent 5e98ed0e11
commit 3dd6a83c68
9 changed files with 492 additions and 188 deletions
+128 -42
View File
@@ -19,7 +19,8 @@ import shutil
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
import ssl
@@ -1081,9 +1082,79 @@ def upload_image_to_postiz(image_path: str) -> dict | None:
return None
def post_to_social(metadata: dict, episode_slug: str, image_path: str = None):
def _build_platform_content(metadata: dict, episode_url: str, yt_url: str | None,
platform: str) -> str:
"""Generate platform-tailored social post content for episode announcements."""
title = metadata["title"]
desc = metadata["description"]
if platform == "x":
hook = desc.split(". ")[0] + "."
content = f"{hook}\n\n{episode_url}\n\n#LukeAtTheRoost #podcast"
if len(content) > 280:
content = f"{title}\n\n{episode_url}"[:280]
elif platform == "instagram":
hashtags = ("#podcast #LukeAtTheRoost #talkradio #callinshow #newepisode "
"#podcastlife #podcastrecommendations #comedy #advice "
"#latenightradio #aipodcast #talkshow")
content = f"New episode 🎙️\n\n{desc}\n\nLink in bio.\n\n{hashtags}"
elif platform == "threads":
content = (f"{title}\n\n{desc}\n\nlukeattheroost.com"
f"\n\n#podcast #LukeAtTheRoost #newepisode #callinshow")
elif platform == "bluesky":
content = f"{desc}\n\n{episode_url}"
if len(content) > 300:
avail = 300 - len(episode_url) - 2
content = desc[:avail].rsplit(" ", 1)[0] + "\n\n" + episode_url
elif platform == "mastodon":
content = f"{title}\n\n{desc}\n\n{episode_url}"
if yt_url:
content += f"\n{yt_url}"
elif platform == "linkedin":
content = f"{title}\n\n{desc}"
content += f"\n\nListen: {episode_url}"
if yt_url:
content += f"\nWatch: {yt_url}"
elif platform == "facebook":
content = f"New episode just dropped 🎙️\n\n{desc}\n\nListen free: {episode_url}"
if yt_url:
content += f"\nWatch: {yt_url}"
elif platform == "tiktok":
hook = desc.split(". ")[0] + "."
content = (f"New episode: {hook}"
f"\n\n#podcast #LukeAtTheRoost #callinshow #newepisode #fyp")
elif platform == "nostr":
content = f"{title}\n\n{desc}\n\n{episode_url}"
if yt_url:
content += f"\n{yt_url}"
else:
content = f"{title}\n\n{desc}\n\n{episode_url}"
return content
# Platforms that post immediately vs scheduled (minutes offset from publish time)
_IMMEDIATE_PLATFORMS = {"x", "bluesky"}
_SCHEDULE_OFFSETS = {
"instagram": 30, "threads": 30,
"facebook": 60, "linkedin": 60,
"tiktok": 90, "mastodon": 120, "nostr": 120,
}
def post_to_social(metadata: dict, episode_slug: str, image_path: str = None,
yt_video_id: str = None):
"""Post episode announcement to all connected social channels via Postiz."""
print("[5.5/5] Posting to social media...")
print("[5.7] Posting to social media...")
token = _get_postiz_token()
@@ -1095,29 +1166,13 @@ def post_to_social(metadata: dict, episode_slug: str, image_path: str = None):
image_ids = [{"id": media["id"], "path": media.get("path", "")}]
episode_url = f"https://lukeattheroost.com/episode.html?slug={episode_slug}"
base_content = f"{metadata['title']}\n\n{metadata['description']}\n\n{episode_url}"
hashtags = "#podcast #LukeAtTheRoost #talkradio #callinshow #newepisode"
hashtag_platforms = {"instagram", "facebook", "bluesky", "mastodon", "nostr", "linkedin", "threads", "tiktok", "x"}
# Platform-specific content length limits
PLATFORM_MAX_LENGTH = {"bluesky": 300, "x": 280, "threads": 500, "tiktok": 2200}
yt_url = f"https://youtube.com/watch?v={yt_video_id}" if yt_video_id else None
now = datetime.now(timezone.utc)
# Post to each platform individually so one failure doesn't block others
posted = 0
for platform, intg_config in POSTIZ_INTEGRATIONS.items():
content = base_content
if platform in hashtag_platforms:
content += f"\n\n{hashtags}"
# Truncate for platforms with short limits
max_len = PLATFORM_MAX_LENGTH.get(platform)
if max_len and len(content) > max_len:
# Keep title + URL, truncate description
short = f"{metadata['title']}\n\n{episode_url}"
if platform in hashtag_platforms:
short += f"\n\n{hashtags}"
content = short[:max_len]
content = _build_platform_content(metadata, episode_url, yt_url, platform)
settings = {"__type": platform, "post_type": "post"}
if platform == "x":
@@ -1131,30 +1186,46 @@ def post_to_social(metadata: dict, episode_slug: str, image_path: str = None):
"settings": settings,
}
# Stagger: immediate for fast-moving platforms, scheduled for rest
offset_min = _SCHEDULE_OFFSETS.get(platform, 0)
if platform in _IMMEDIATE_PLATFORMS or offset_min == 0:
post_type = "now"
post_date = now.strftime("%Y-%m-%dT%H:%M:%S.000Z")
else:
post_type = "schedule"
scheduled = now + timedelta(minutes=offset_min)
post_date = scheduled.strftime("%Y-%m-%dT%H:%M:%S.000Z")
payload = {
"type": "now",
"type": post_type,
"shortLink": False,
"date": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"),
"date": post_date,
"tags": [],
"posts": [post],
}
try:
resp = requests.post(
f"{POSTIZ_URL}/api/posts",
headers={"auth": token, "Content-Type": "application/json"},
json=payload,
timeout=60,
)
if resp.status_code in (200, 201):
posted += 1
print(f" Posted to {platform}")
else:
print(f" Warning: {platform} failed ({resp.status_code}): {resp.text[:150]}")
except Exception as e:
print(f" Warning: {platform} failed: {e}")
# Retry once on failure (2 attempts, 5s backoff)
for attempt in range(2):
try:
resp = requests.post(
f"{POSTIZ_URL}/api/posts",
headers={"auth": token, "Content-Type": "application/json"},
json=payload,
timeout=60,
)
if resp.status_code in (200, 201):
posted += 1
label = f"scheduled +{offset_min}m" if post_type == "schedule" else "posted"
print(f" {platform}: {label}")
break
else:
print(f" Warning: {platform} attempt {attempt + 1} failed ({resp.status_code}): {resp.text[:150]}")
except Exception as e:
print(f" Warning: {platform} attempt {attempt + 1} failed: {e}")
if attempt < 1:
time.sleep(5)
print(f" Posted to {posted}/{len(POSTIZ_INTEGRATIONS)} channels")
print(f" Posted/scheduled {posted}/{len(POSTIZ_INTEGRATIONS)} channels")
def get_youtube_service():
@@ -1201,6 +1272,21 @@ def _check_youtube_duplicate(youtube, title: str) -> str | None:
return None
def _extract_youtube_tags(metadata: dict) -> list[str]:
"""Extract dynamic tags from episode metadata for YouTube SEO."""
base_tags = ["podcast", "Luke at the Roost", "talk radio", "call-in show",
"talk show", "comedy", "AI podcast", "late night radio", "advice"]
skip = {"intro", "outro", "opening", "closing", "wrap up", "wrap-up"}
dynamic = []
for ch in metadata.get("chapters", []):
title = ch.get("title", "").strip()
if title.lower() in skip or len(title) < 3:
continue
if len(title) <= 50:
dynamic.append(title)
return (base_tags + dynamic)[:25]
def upload_to_youtube(audio_path: str, metadata: dict, chapters: list,
episode_slug: str) -> str | None:
"""Convert audio to video with cover art, upload to YouTube, add to podcast playlist."""
@@ -1259,8 +1345,7 @@ def upload_to_youtube(audio_path: str, metadata: dict, chapters: list,
"snippet": {
"title": metadata["title"][:100],
"description": description,
"tags": ["podcast", "Luke at the Roost", "talk radio", "call-in show",
"talk show", "comedy"],
"tags": _extract_youtube_tags(metadata),
"categoryId": "22",
},
"status": {
@@ -1631,7 +1716,8 @@ def main():
else:
social_image_path = str(audio_path.with_suffix(".social.jpg"))
generate_social_image(episode_number, metadata["description"], social_image_path)
post_to_social(metadata, episode["slug"], social_image_path)
post_to_social(metadata, episode["slug"], social_image_path,
yt_video_id=yt_video_id)
_mark_step_done(episode_number, "social")
# Step 6: Summary