From 96ac9908050f845f39d810735f504a8c6b92c404 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Fri, 7 Aug 2026 12:30:52 -0700 Subject: [PATCH] Serve KidLisp backdrops from the oven and guard its env Daily 1024px animated-webp grabs promote to a stable key and redirect through the CDN; the recap git poller only runs when RECAP_POLL_INTERVAL_MS is set; deploys source the vault .env and keep rsync --delete away from it. --- oven/deploy.sh | 22 ++++++++++++++++ oven/grabber.mjs | 55 ++++++++++++++++++++++++++++++--------- oven/recap-git-poller.mjs | 9 +++++-- oven/server.mjs | 2 +- 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/oven/deploy.sh b/oven/deploy.sh index ca9cf02b1..91da01ffe 100755 --- a/oven/deploy.sh +++ b/oven/deploy.sh @@ -12,6 +12,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" AC_SOURCE="$SCRIPT_DIR/../system/public/aesthetic.computer" FEDAC_SOURCE="$SCRIPT_DIR/../fedac" VAULT_OS_KEY="$SCRIPT_DIR/../aesthetic-computer-vault/oven/os-build-admin-key.txt" +VAULT_ENV="$SCRIPT_DIR/../aesthetic-computer-vault/oven/.env" # Millisecond epoch — portable (macOS `date +%N` doesn't exist, GNU does). ms() { python3 -c 'import time; print(int(time.time()*1000))'; } @@ -32,6 +33,7 @@ echo "📦 Syncing oven files..." rsync -avz --progress --delete \ --exclude='node_modules' \ --exclude='.git' \ + --exclude='.env' \ --exclude='*.log' \ --exclude='ac-source' \ --exclude='native-git' \ @@ -45,6 +47,26 @@ SYNC_TIME=$((END_SYNC - START_TIME)) echo "" echo "✅ Oven sync complete in ${SYNC_TIME}ms" +# The source tree intentionally does not contain .env. Keep rsync --delete from +# removing production credentials, then refresh them from the local vault when +# it is available. Runtime-managed values are appended later in this script. +if [ -f "$VAULT_ENV" ]; then + echo "🔐 Syncing Oven environment from vault..." + rsync -az \ + -e "ssh -i $SSH_KEY -o StrictHostKeyChecking=no" \ + "$VAULT_ENV" \ + "root@$OVEN_HOST:$REMOTE_DIR/.env" + ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "root@$OVEN_HOST" " +chmod 600 $REMOTE_DIR/.env +if id -u oven >/dev/null 2>&1; then + chown oven:oven $REMOTE_DIR/.env +fi +" + echo "✅ Oven environment synced" +else + echo "⚠️ No vault environment at $VAULT_ENV; preserving remote .env" +fi + # Sync aesthetic.computer source files needed for bundle generation echo "" echo "📦 Syncing ac-source files for bundler..." diff --git a/oven/grabber.mjs b/oven/grabber.mjs index 1a56a66d2..d7d409fd4 100644 --- a/oven/grabber.mjs +++ b/oven/grabber.mjs @@ -4421,7 +4421,7 @@ const backdropCache = { }; /** - * Get or generate KidLisp backdrop - a 2048px animated webp of a featured piece. + * Get or generate KidLisp backdrop - a 1024px animated webp of a featured piece. * Rotates daily based on top hits. Caches to CDN for fast access. * @param {boolean} force - Force regeneration even if cached * @returns {{ url: string, piece: string, cached: boolean }} @@ -4488,16 +4488,33 @@ export async function generateKidlispBackdrop(force = false) { throw new Error(result.error || 'Failed to generate backdrop'); } - // grabPiece returns cdnUrl from oven/grabs/ - use that directly - const url = result.cdnUrl; - - if (!url) { - throw new Error('No CDN URL returned from grabPiece'); + // Promote the generated grab to a stable daily key. A cached grab may not + // include its buffer, so retrieve it before publishing the canonical object. + let buffer = result.buffer; + if (!buffer && result.cdnUrl) { + const response = await fetch(result.cdnUrl); + if (!response.ok) { + throw new Error(`Failed to read cached backdrop: ${response.status}`); + } + buffer = Buffer.from(await response.arrayBuffer()); } - - console.log(`📤 Backdrop generated: ${url}`); - - // Update cache with the grab URL + if (!buffer) { + throw new Error('No backdrop bytes returned from grabPiece'); + } + + await spacesClient.send(new PutObjectCommand({ + Bucket: SPACES_BUCKET, + Key: key, + Body: buffer, + ContentType: 'image/webp', + ACL: 'public-read', + CacheControl: 'public, max-age=31536000, immutable', + })); + + const url = `${SPACES_CDN_BASE}/${key}`; + console.log(`📤 Backdrop published: ${url}`); + + // Update cache with the canonical daily URL backdropCache.url = url; backdropCache.date = today; backdropCache.piece = piece; @@ -4507,7 +4524,7 @@ export async function generateKidlispBackdrop(force = false) { /** * Get cached backdrop URL without triggering generation. - * Returns the in-memory cached URL if available for today, null otherwise. + * Returns today's durable Spaces URL if available, null otherwise. */ export async function getLatestBackdropUrl() { const today = getTodayKey(); @@ -4517,8 +4534,20 @@ export async function getLatestBackdropUrl() { return backdropCache.url; } - // No cached URL for today - caller should trigger generation - return null; + const key = `backdrop/kidlisp/${today}.webp`; + try { + await spacesClient.send(new HeadObjectCommand({ + Bucket: SPACES_BUCKET, + Key: key, + })); + const url = `${SPACES_CDN_BASE}/${key}`; + backdropCache.url = url; + backdropCache.date = today; + return url; + } catch { + // No durable object for today - caller should trigger generation. + return null; + } } // ============================================================================= diff --git a/oven/recap-git-poller.mjs b/oven/recap-git-poller.mjs index e342afc27..357ec5f1c 100644 --- a/oven/recap-git-poller.mjs +++ b/oven/recap-git-poller.mjs @@ -1,7 +1,7 @@ // recap-git-poller.mjs — polls git for recap/audience/*.mjs changes, // auto-triggers recap mp4 builds via startRecapBuild. // -// Runs inside the oven server. Every POLL_INTERVAL_MS (default 90s), +// Runs inside the oven server when RECAP_POLL_INTERVAL_MS is explicitly set. // fetches origin/main and checks if any audience config changed since // the last successful build. If so, pulls and triggers startRecapBuild // for each changed audience (one at a time — recap-builder serializes). @@ -13,7 +13,7 @@ import { execFile } from "child_process"; import { promises as fs } from "fs"; import path from "path"; -const POLL_INTERVAL_MS = parseInt(process.env.RECAP_POLL_INTERVAL_MS || "90000", 10); +const POLL_INTERVAL_MS = parseInt(process.env.RECAP_POLL_INTERVAL_MS || "0", 10); const GIT_REPO_DIR = process.env.NATIVE_GIT_DIR || "/opt/oven/native-git"; const BRANCH = process.env.NATIVE_GIT_BRANCH || "main"; const HASH_FILE = path.join(GIT_REPO_DIR, ".last-recap-built-hash"); @@ -139,6 +139,11 @@ export function startPoller({ startRecapBuild, addServerLog }) { startBuildFn = startRecapBuild; if (addServerLog) logFn = addServerLog; + if (!Number.isFinite(POLL_INTERVAL_MS) || POLL_INTERVAL_MS <= 0) { + logFn("info", "⏭️", "Recap git poller disabled (set RECAP_POLL_INTERVAL_MS to enable)"); + return; + } + fs.access(GIT_REPO_DIR) .then(() => { logFn("info", "🎬", `Recap git poller started (every ${POLL_INTERVAL_MS / 1000}s, repo: ${GIT_REPO_DIR})`); diff --git a/oven/server.mjs b/oven/server.mjs index c96da674d..81d234203 100644 --- a/oven/server.mjs +++ b/oven/server.mjs @@ -2054,7 +2054,7 @@ app.get('/news-og/:code.png', async (req, res) => { // KidLisp Backdrop - Animated WebP for login screens, Auth0, etc. // ============================================================================= -// Fast redirect to CDN-cached 2048px animated webp +// Fast redirect to CDN-cached 1024px animated webp app.get('/kidlisp-backdrop.webp', async (req, res) => { try { // Get cached URL without triggering generation (fast!) -- 2.51.2