Fix memories disappearing + always-processing state

Three bugs fixed:

1. Vision failure was marking memories as 'failed', triggering orphan
   cleanup that permanently deleted them. Vision is optional — on error,
   now marks 'ready' so the image stays visible without AI captions.

2. Thumbnail failure was blocking vision from running (rejected promise
   swallowed the chain). Fixed by catching thumbnail error separately so
   processMemoryVision always executes and always sets a final status.

3. /api/img proxy was rejecting thumbnail keys (families/{id}/thumbnails/…)
   because 'families/' was not in ALLOWED_PREFIXES. Added it.

Also: replaced the full-bleed 'Processing…' overlay with a small corner
badge so the uploaded photo is visible immediately after upload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-28 22:37:46 +05:30
parent a3a0ddf3c9
commit f953963b3b
4 changed files with 10 additions and 6 deletions

View file

@ -563,9 +563,9 @@ function MemoryTile({ memory, folder, onClick }: { memory: Memory; folder: Folde
<span className="text-white text-lg drop-shadow"></span> <span className="text-white text-lg drop-shadow"></span>
</div> </div>
{memory.processingStatus === "processing" && ( {memory.processingStatus === "processing" && (
<div className="absolute inset-0 bg-black/40 flex items-center justify-center rounded-xl"> <span className="absolute top-1 left-1 text-[8px] bg-black/60 text-white px-1.5 py-0.5 rounded-full leading-tight">
<span className="text-white text-[9px] bg-black/50 px-1.5 py-0.5 rounded-full">Processing</span>
</div> </span>
)} )}
{memory.isPrivate && <span className="absolute top-1 right-1 text-[9px]">🔒</span>} {memory.isPrivate && <span className="absolute top-1 right-1 text-[9px]">🔒</span>}
</button> </button>

View file

@ -1,7 +1,7 @@
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
const ALLOWED_PREFIXES = ["avatars/", "profiles/", "memories/", "thumbnails/"]; const ALLOWED_PREFIXES = ["avatars/", "profiles/", "memories/", "thumbnails/", "families/"];
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const key = req.nextUrl.searchParams.get("key"); const key = req.nextUrl.searchParams.get("key");

View file

@ -84,10 +84,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
} }
} }
// Mark as processing and start the media pipeline // Mark as processing and start the media pipeline.
// thumbnail failure is non-fatal — always proceed to vision, which marks 'ready'.
await sql`UPDATE memories SET processing_status = 'processing', updated_at = now() WHERE id = ${id}`; await sql`UPDATE memories SET processing_status = 'processing', updated_at = now() WHERE id = ${id}`;
generateThumbnail(id) generateThumbnail(id)
.catch(e => console.error(`[thumbnail] id=${id}`, e)) // swallow so vision always runs
.then(() => processMemoryVision(id)) .then(() => processMemoryVision(id))
.catch(e => console.error(`[memory pipeline] id=${id}`, e)); .catch(e => console.error(`[memory pipeline] id=${id}`, e));

View file

@ -125,7 +125,9 @@ export async function processMemoryVision(memoryId: string): Promise<void> {
`; `;
} catch (e) { } catch (e) {
console.error(`[vision] Failed for memory ${memoryId}:`, e); console.error(`[vision] Failed for memory ${memoryId}:`, e);
await sql`UPDATE memories SET processing_status = 'failed', updated_at = now() WHERE id = ${memoryId}`; // Vision is optional — mark as ready so the image is still visible.
// Only the confirm step (HeadObject failure) should mark as 'failed'.
await sql`UPDATE memories SET processing_status = 'ready', updated_at = now() WHERE id = ${memoryId}`;
await logAudit({ await logAudit({
action: "vision_processing_failed", action: "vision_processing_failed",
metadata: { memoryId, error: String(e) }, metadata: { memoryId, error: String(e) },