- Notifications
You must be signed in to change notification settings - Fork 0
feat: implement deferred asset and image optimization items#1313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
e8dc2b3edbf3296780f09a283050File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| ⨯ Another next dev server is already running. | ||
| - Local: http://localhost:4500 | ||
| - PID: 104364 | ||
| - Dir: C:\Dev\Apps\Database | ||
| - Log: .next\dev\logs\next-development.log | ||
| Run taskkill /PID 104364 /F to stop it. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| Starting Clinical KB at http://localhost:4510 (configured port). | ||
| ▲ Next.js 16.2.11 (webpack) | ||
| - Local: http://localhost:4510 | ||
| - Network: http://0.0.0.0:4510 | ||
| - Environments: .env.local | ||
| ✓ Ready in 13.4s○ Compiling /instrumentation ... | ||
| - Experiments (use with caution): | ||
| · cpus: 1 | ||
| · optimizePackageImports | ||
| · proxyClientMaxBodySize: "151mb" | ||
| ○ Compiling proxy ... | ||
| ○ Compiling /api/local-project-id ... | ||
| ○ Compiling / ... | ||
| ○ Compiling /api/local-project-id ... | ||
| GET /api/local-project-id 200 in 444ms (next.js: 100ms, proxy.ts: 136ms, application-code: 208ms) | ||
| GET /api/local-project-id 200 in 32ms (next.js: 6ms, proxy.ts: 10ms, application-code: 16ms) | ||
| GET /api/local-project-id 200 in 88ms (next.js: 11ms, proxy.ts: 29ms, application-code: 48ms) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,59 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from "node:fs/promises"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import sharp from "sharp"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DIRECTORIES = ["public/mockups", "public/demo-documents"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function walkDir(dir) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let results = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const list = await fs.readdir(dir); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const file of list) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const filePath = path.join(dir, file); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stat = await fs.stat(filePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (stat && stat.isDirectory()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| results = results.concat(await walkDir(filePath)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (filePath.toLowerCase().endsWith(".png")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| results.push(filePath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (err.code !== "ENOENT") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Error reading directory ${dir}:`, err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return results; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function optimizeImages() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Optimizing public images to WebP and AVIF..."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let totalFiles = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const dir of DIRECTORIES) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pngFiles = await walkDir(dir); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const file of pngFiles) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ext = path.extname(file); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const base = file.slice(0, -ext.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const webpPath = `${base}.webp`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const avifPath = `${base}.avif`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await sharp(file).webp({ quality: 80 }).toFile(webpPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await sharp(file).avif({ quality: 80 }).toFile(avifPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalFiles++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Optimized: ${file}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Failed to optimize ${file}:`, err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If WebP generation succeeds but AVIF generation fails—or either conversion fails outright—the per-file catch only logs the error, leaves any partial output behind, and allows Useful? React with 👍 / 👎. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Successfully generated variants for ${totalFiles} images.`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+43
to
+53
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Fail the command when any variant cannot be generated. A conversion failure is logged but swallowed, so CI exits Proposed fix async function optimizeImages() {
console.log("Optimizing public images to WebP and AVIF...");
-+
let totalFiles = 0;
+ let failedFiles = 0;
for (const dir of DIRECTORIES) {
const pngFiles = await walkDir(dir);
-+
for (const file of pngFiles) {
@@
} catch (err) {
+ failedFiles++;
console.error(`Failed to optimize ${file}:`, err);
}
}
}
+ if (failedFiles > 0) {+ throw new Error(`Failed to generate variants for ${failedFiles} image(s).`);+ }
console.log(`Successfully generated variants for ${totalFiles} images.`);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| optimizeImages().catch((err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Image optimization failed:", err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -15,7 +15,7 @@ export const runtime = "nodejs"; | ||
| const signedUrlTtlSeconds = env.DOCUMENT_SIGNED_URL_TTL_SECONDS; | ||
| const routeIdSchema = z.string().uuid(); | ||
| export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) { | ||
| export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { | ||
| try { | ||
| const { id } = await params; | ||
| if (isDemoMode()) { | ||
| @@ -33,7 +33,7 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id: | ||
| if (!routeIdSchema.safeParse(id).success) throw new PublicApiError("Invalid image id."); | ||
| const supabase = createAdminClient(); | ||
| const { access, rateLimit } = await enforceDocumentReadRateLimit(_request, supabase); | ||
| const { access, rateLimit } = await enforceDocumentReadRateLimit(request, supabase); | ||
| if (rateLimit.limited) { | ||
| return rateLimitJsonResponse("Document requests are rate limited. Try again shortly.", rateLimit); | ||
| } | ||
| @@ -62,9 +62,15 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id: | ||
| return NextResponse.json({ error: "Image not found." }, { status: 404 }); | ||
| } | ||
| const { searchParams } = new URL(request.url); | ||
| const widthParam = searchParams.get("w"); | ||
| const transform = widthParam | ||
| ? { width: parseInt(widthParam, 10), resize: "contain" as const } | ||
| : undefined; | ||
Comment on lines
+67
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When AGENTS.md reference: AGENTS.md:L551-L559 Useful? React with 👍 / 👎. | ||
| const signed = await supabase.storage | ||
| .from(env.SUPABASE_IMAGE_BUCKET) | ||
| .createSignedUrl(image.storage_path, signedUrlTtlSeconds); | ||
| .createSignedUrl(image.storage_path, signedUrlTtlSeconds, { transform }); | ||
Comment on lines
+65
to
+73
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bashset -euo pipefail
file="src/app/api/images/[id]/signed-url/route.ts"echo"== file info =="
wc -l "$file"echoecho"== outline =="
ast-grep outline "$file" --view expanded ||trueechoecho"== relevant lines =="
sed -n '1,220p'"$file"| cat -nRepository: BigSimmo/Database Length of output: 4634 🏁 Script executed: #!/bin/bashset -euo pipefail
echo"== search for this signed-url route and w parameter =="
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build''signed-url|[?&]w=|searchParams.get\("w"\)|createSignedUrl\(' src .||trueRepository: BigSimmo/Database Length of output: 17393 🏁 Script executed: #!/bin/bashset -euo pipefail
file="src/components/clinical-dashboard/signed-image.tsx"echo"== outline =="
ast-grep outline "$file" --view expanded ||trueechoecho"== relevant lines =="
sed -n '1,240p'"$file"| cat -nRepository: BigSimmo/Database Length of output: 10369 🏁 Script executed: #!/bin/bashset -euo pipefail
echo"== tests or docs mentioning thumbnail width =="
rg -n 'w=400|thumbnail|optimized thumbnail|createSignedUrl\(.*transform|resize: "contain"' \
src tests docs | sed -n '1,200p'Repository: BigSimmo/Database Length of output: 1070 🌐 Web query:
💡 Result: When using the Citations:
Validate and bound 🤖 Prompt for AI Agents
Comment on lines
71
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For every existing request without AGENTS.md reference: AGENTS.md:L553-L559 Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For every existing request without Useful? React with 👍 / 👎. | ||
| if (signed.error) throw new Error(signed.error.message); | ||
| return NextResponse.json({ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -290,16 +290,23 @@ export async function POST(request: Request) { | ||
| insertedDocumentOwnerId = uploadOwnerId; | ||
| assertUploadNotAborted(request); | ||
| await writeAuditLog(supabase, { | ||
| ownerId: uploadOwnerId, | ||
| action: "document_upload", | ||
| resourceType: "document", | ||
| resourceId: documentId, | ||
| // `audit_logs` is retained indefinitely. Keep only operational facts there; | ||
| // the user-controlled filename and content hash remain on the scoped document | ||
| // record, not in the durable audit trail. | ||
| metadata: { fileType: file.type, fileSize: file.size }, | ||
| }); | ||
| try { | ||
| await writeAuditLog(supabase, { | ||
| ownerId: uploadOwnerId, | ||
| action: "document_upload", | ||
| resourceType: "document", | ||
| resourceId: documentId, | ||
| // `audit_logs` is retained indefinitely. Keep only operational facts there; | ||
| // the user-controlled filename and content hash remain on the scoped document | ||
| // record, not in the durable audit trail. | ||
| metadata: { fileType: file.type, fileSize: file.size }, | ||
| }); | ||
| } catch (auditError) { | ||
| logger.warn("Upload succeeded but audit log failed", { | ||
| documentId, | ||
| message: auditError instanceof Error ? auditError.message : String(auditError), | ||
| }); | ||
| } | ||
Comment on lines
+293
to
+309
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Ensure audit failures reach this handler if the document-scoped warning is required.
🤖 Prompt for AI Agents | ||
| return NextResponse.json({ document, job }, { status: 201 }); | ||
| } catch (error) { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -32,6 +32,8 @@ export const SignedImage = memo(function SignedImage({ | ||
| zoomable = false, | ||
| caption, | ||
| aspectRatio, | ||
| thumbnail = false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because Useful? React with 👍 / 👎. | ||
| placeholderBase64, | ||
| }: { | ||
| /** Signed-URL API route, e.g. `/api/images/{id}/signed-url`. */ | ||
| endpoint: string; | ||
| @@ -51,13 +53,20 @@ export const SignedImage = memo(function SignedImage({ | ||
| caption?: string; | ||
| /** Optional intrinsic width/height ratio for document crops that are not 4:3. */ | ||
| aspectRatio?: number | null; | ||
| /** If true, appends ?w=400 to request a smaller optimized thumbnail. */ | ||
| thumbnail?: boolean; | ||
| /** Tiny base64 Blurhash/LQIP placeholder to show while loading. */ | ||
| placeholderBase64?: string | null; | ||
| }) { | ||
| const [shouldLoad, setShouldLoad] = useState(() => Boolean(getCachedSignedUrl(endpoint))); | ||
| const [loaded, setLoaded] = useState(false); | ||
| const [lightboxOpen, setLightboxOpen] = useState(false); | ||
| const frameRef = useRef<HTMLDivElement | null>(null); | ||
| const triggerRef = useRef<HTMLButtonElement>(null); | ||
| const { url, failed, retry, markFailed } = useSignedImageUrl(endpoint, shouldLoad); | ||
| const resolvedEndpoint = thumbnail | ||
| ? endpoint.includes("?") ? `${endpoint}&w=400` : `${endpoint}?w=400` | ||
| : endpoint; | ||
| const { url, failed, retry, markFailed } = useSignedImageUrl(resolvedEndpoint, shouldLoad); | ||
Comment on lines
+56
to
+69
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win Use the resolved endpoint for cache gating.
🤖 Prompt for AI Agents🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Replace an existing If 🤖 Prompt for AI Agents | ||
| // Defer the request until the frame is near the viewport. A cached URL seeds | ||
| // `shouldLoad` synchronously, so already-fetched images skip the observer. | ||
| @@ -187,9 +196,17 @@ export const SignedImage = memo(function SignedImage({ | ||
| </> | ||
| ) : null} | ||
| {!url || !loaded ? ( | ||
| <div className="absolute inset-0 flex items-center justify-center text-center text-xs font-semibold text-[color:var(--text-muted)]"> | ||
| <div className="absolute inset-0 flex items-center justify-center text-center text-xs font-semibold text-[color:var(--text-muted)] overflow-hidden"> | ||
| {shouldLoad ? ( | ||
| <Skeleton className="absolute inset-0 h-full w-full rounded-none" /> | ||
| placeholderBase64 ? ( | ||
| <div | ||
| className="absolute inset-0 h-full w-full bg-cover bg-center bg-no-repeat opacity-50 blur-md transform scale-110" | ||
| style={{ backgroundImage: `url(${placeholderBase64})` }} | ||
| aria-hidden="true" | ||
| /> | ||
| ) : ( | ||
| <Skeleton className="absolute inset-0 h-full w-full rounded-none" /> | ||
| ) | ||
| ) : ( | ||
| <div className="grid place-items-center gap-1">Image preview will load when visible</div> | ||
| )} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto"; | ||
| import { readFile, rm } from "node:fs/promises"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import sharp from "sharp"; | ||
| import { env } from "../src/lib/env"; | ||
| import { buildChunks } from "../src/lib/chunking"; | ||
| import { ragEnrichmentVersion, upsertDocumentEnrichment } from "../src/lib/document-enrichment"; | ||
| @@ -1127,6 +1128,18 @@ async function uploadAndCaptionImages( | ||
| const ext = path.extname(image.path) || ".png"; | ||
| const bytes = await readFile(image.path); | ||
| let placeholderBase64 = null; | ||
| try { | ||
| const placeholderBuffer = await sharp(bytes) | ||
| .resize(16, null, { withoutEnlargement: true }) | ||
| .webp({ quality: 20 }) | ||
| .toBuffer(); | ||
| placeholderBase64 = `data:image/webp;base64,${placeholderBuffer.toString("base64")}`; | ||
Comment on lines
+1134
to
+1138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Every retained ingestion image is now decoded and WebP-encoded, but AGENTS.md reference: AGENTS.md:L551-L559 Useful? React with 👍 / 👎. | ||
| } catch (e) { | ||
| console.warn(`Failed to generate placeholder for ${image.path}`, e); | ||
| } | ||
| const imagePrefix = job.documents.owner_id | ||
| ? `${job.documents.owner_id}/images/${job.document_id}` | ||
| : `local/${job.document_id}`; | ||
| @@ -1157,6 +1170,7 @@ async function uploadAndCaptionImages( | ||
| labels: classification.labels.map(cleanString), | ||
| metadata: sanitizeJsonbRecord({ | ||
| ...(image.metadata ?? {}), | ||
| placeholder_base64: placeholderBase64, | ||
| extractor: "local-worker", | ||
| index_generation_id: indexGenerationId, | ||
| image_hash: imageHash, | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove the captured development-server log.
This generated artifact exposes local machine details and a stale PID command without supporting the mockup. Delete it from the change.
🤖 Prompt for AI Agents