Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,7 +202,8 @@
"drift:manifest": "node scripts/run-tsx.mjs scripts/generate-drift-manifest.ts",
"sync:pr-branches": "node scripts/sync-open-pr-branches.mjs",
"sync:pr-branches:apply": "node scripts/sync-open-pr-branches.mjs --apply",
"check:assets": "node scripts/check-assets.mjs"
"check:assets": "node scripts/check-assets.mjs",
"optimize:images": "node scripts/optimize-public-images.mjs"
},
"dependencies": {
"@next/env": "16.2.11",
Expand All@@ -221,6 +222,7 @@
"react": "19.2.7",
"react-dom": "19.2.7",
"server-only": "^0.0.1",
"sharp": "0.35.3",
"zod": "^4.4.3"
},
"overrides": {
Expand Down
8 changes: 8 additions & 0 deletions phone-mockup-dev.err
Original file line numberDiff line numberDiff 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.
Comment on lines +1 to +8

Copy link
Copy Markdown
Contributor

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@phone-mockup-dev.err` around lines 1 - 8, Remove the captured
development-server log artifact from the change, including its local paths, PID,
and taskkill command; do not modify the mockup implementation.

18 changes: 18 additions & 0 deletions phone-mockup-dev.log
Original file line numberDiff line numberDiff 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)
59 changes: 59 additions & 0 deletions scripts/optimize-public-images.mjs
Original file line numberDiff line numberDiff 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serve the variants produced by the optimizer

When npm run optimize:images is executed, these calls create sibling WebP and AVIF files, but every application reference under the targeted directories still points to the PNG files and there is no picture/source or format-selection logic. The command therefore produces unused artifacts without reducing any served image payload; update the consumers or generated paths to select these variants and add a check proving that an optimized asset is actually referenced.

Useful? React with 👍 / 👎.

totalFiles++;
console.log(`Optimized: ${file}`);
} catch (err) {
console.error(`Failed to optimize ${file}:`, err);
}
Comment on lines +48 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail the optimizer when any conversion fails

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 npm run optimize:images to exit successfully. Automation and developers therefore cannot distinguish a complete asset set from a partial one; record failures and return a nonzero exit status after processing, with a focused test that makes one Sharp conversion reject.

Useful? React with 👍 / 👎.

}
}
console.log(`Successfully generated variants for ${totalFiles} images.`);
Comment on lines +43 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 0 and reports success while variants may be missing or stale. Aggregate failures and throw before the success message.

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

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try{
awaitsharp(file).webp({quality: 80}).toFile(webpPath);
awaitsharp(file).avif({quality: 80}).toFile(avifPath);
totalFiles++;
console.log(`Optimized: ${file}`);
}catch(err){
console.error(`Failed to optimize ${file}:`,err);
}
}
}
console.log(`Successfully generated variants for ${totalFiles} images.`);
asyncfunctionoptimizeImages(){
console.log("Optimizing public images to WebP and AVIF...");
lettotalFiles=0;
letfailedFiles=0;
for(constdirofDIRECTORIES){
constpngFiles=awaitwalkDir(dir);
for(constfileofpngFiles){
try{
awaitsharp(file).webp({quality: 80}).toFile(webpPath);
awaitsharp(file).avif({quality: 80}).toFile(avifPath);
totalFiles++;
console.log(`Optimized: ${file}`);
}catch(err){
failedFiles++;
console.error(`Failed to optimize ${file}:`,err);
}
}
}
if(failedFiles>0){
thrownewError(`Failed to generate variants for ${failedFiles} image(s).`);
}
console.log(`Successfully generated variants for ${totalFiles} images.`);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/optimize-public-images.mjs` around lines 43 - 53, Update the
image-processing loop around the per-file try/catch to collect conversion
failures instead of swallowing them, while retaining the existing error logging.
After processing all files, throw an error if any failures were recorded, before
printing the “Successfully generated variants” message; only report success when
every WebP and AVIF variant was generated.

}

optimizeImages().catch((err) => {
console.error("Image optimization failed:", err);
process.exit(1);
});
12 changes: 9 additions & 3 deletions src/app/api/images/[id]/signed-url/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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()) {
Expand All@@ -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);
}
Expand DownExpand Up@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid thumbnail widths before signing

When w is non-numeric, zero, negative, partially numeric, or excessively large, parseInt produces NaN or an out-of-range value that is passed directly to Supabase instead of returning a client error. Requests such as ?w=abc or ?w=-1 can therefore produce a provider error or an unusable transformed URL; validate a bounded positive integer before constructing transform, with focused route tests for malformed and boundary values.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 -n

Repository: 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 .||true

Repository: 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 -n

Repository: 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:

Supabase storage createSignedUrl transform width maximum image resize contain invalid width NaN behavior docs

💡 Result:

When using the createSignedUrl method in Supabase Storage with image transformations, providing an invalid width value, such as NaN, does not trigger specific client-side error handling for the NaN value itself [1][2]. Instead, the behavior depends on how the underlying transformation service interprets the input. In the Supabase Storage architecture, the transformation service (often imgproxy or internal handlers) expects valid numeric values for dimensions [3][4]. If a NaN value is passed, the request may fail to produce a valid image because the transformation parameters will be malformed [4]. Key behavioral points include: 1. Client-Side Handling: The Supabase SDKs typically pass the transformation options object to the storage API [2]. The SDK does not inherently sanitize or strip NaN values before transmission [1][5]. 2. Server-Side Impact: When the storage server receives a NaN width, it cannot perform the requested resize [4]. Depending on the specific deployment and configuration, this often results in a 4xx series error (such as a 400 Bad Request) from the storage-api or the underlying transformation engine, as the requested image cannot be rendered [4]. 3. Recommended Practice: You should ensure that any values passed to the transform object are validated as finite numbers before calling createSignedUrl. Use Number.isFinite() or similar checks to ensure width and height are valid integers. Historical context: Recent updates to the Supabase JavaScript SDK (around March/April 2026) have improved how transform objects are handled, particularly ensuring that empty transformation objects do not inadvertently cause the system to route requests to the wrong render endpoint [6][7][5]. However, these fixes do not change the requirement for valid numerical input when a transformation is actually requested. [5]

Citations:


Validate and bound w before calling createSignedUrl.parseInt accepts malformed values like 12px and abc, and this route only needs thumbnail sizes, so reject non-integers and cap the width at 400 to avoid malformed transform requests and unnecessary image-processing load.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/api/images/`[id]/signed-url/route.ts around lines 65 - 73, Validate
widthParam before constructing transform in the signed URL route: require a
complete positive integer value, reject malformed or non-integer inputs instead
of relying on parseInt, and enforce a maximum width of 400. Preserve transform
as undefined when w is absent, and return the route’s established client-error
response for invalid values before calling createSignedUrl.

Comment on lines 71 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the no-transform signed URL contract

For every existing request without w, this now calls createSignedUrl(path, ttl, { transform: undefined }), while the unchanged ownership, public-document, and legacy-generation cases in tests/private-access-routes.test.ts assert the established two-argument call. Those focused tests will fail even though no transform was requested; conditionally omit the options argument or update the contract tests if the three-argument shape is intentional.

AGENTS.md reference: AGENTS.md:L553-L559

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the no-transform signed URL call contract

For every existing request without ?w=..., this now invokes createSignedUrl(path, ttl, { transform: undefined }); the focused route tests at tests/private-access-routes.test.ts lines 1117, 1147, and 1176 assert the established two-argument call, so those tests fail before the thumbnail path is exercised. Pass the options object only when a validated width exists, or update the existing expectations alongside a new transformed-width case.

Useful? React with 👍 / 👎.


if (signed.error) throw new Error(signed.error.message);
return NextResponse.json({
Expand Down
27 changes: 17 additions & 10 deletions src/app/api/upload/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

writeAuditLog already catches and logs both Supabase errors and thrown exceptions in src/lib/audit.ts:62-80, then resolves successfully. Consequently, this catch is not entered for actual audit-log failures, so the promised warning containing documentId is never emitted. Either make writeAuditLog return a failure result/throw after logging, or move the document-scoped warning into that helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/api/upload/route.ts` around lines 293 - 309, The document-scoped
audit warning in the upload handler is unreachable because writeAuditLog absorbs
failures. Update writeAuditLog to propagate failures after its existing logging,
or move the warning into that helper, ensuring audit failures emit a warning
containing documentId while preserving successful uploads.


return NextResponse.json({ document, job }, { status: 201 });
} catch (error) {
Expand Down
3 changes: 3 additions & 0 deletions src/app/layout.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import type { Metadata, Viewport } from "next";
import localFont from "next/font/local";
import { cookies, headers } from "next/headers";
import ReactDOM from "react-dom";
import { AuthProvider } from "@/lib/supabase/client";
import { AccountDataProvider } from "@/components/account-data-provider";
import { PwaLifecycle } from "@/components/pwa-lifecycle";
Expand DownExpand Up@@ -85,6 +86,8 @@ export default async function RootLayout({
const isDark = clinicalTheme === "dark";
const themeClass = isDark ? "dark" : "";

ReactDOM.preload("/icon.svg", { as: "image", type: "image/svg+xml", fetchPriority: "high" });

return (
<html
lang="en-AU"
Expand Down
23 changes: 20 additions & 3 deletions src/components/clinical-dashboard/signed-image.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,8 @@ export const SignedImage = memo(function SignedImage({
zoomable = false,
caption,
aspectRatio,
thumbnail = false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enable thumbnails at existing image call sites

Because thumbnail defaults to false and no SignedImage call site in this commit sets it, existing document and evidence previews continue requesting the unmodified endpoint and downloading full-resolution images; the new ?w=400 signing path is therefore unused by the application. Enable this for inline previews while retaining the original endpoint for the lightbox, and add a focused render test asserting that SourceImage or DocumentImage fetches a width-qualified URL.

Useful? React with 👍 / 👎.

placeholderBase64,
}: {
/** Signed-URL API route, e.g. `/api/images/{id}/signed-url`. */
endpoint: string;
Expand All@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Use the resolved endpoint for cache gating.

shouldLoad checks the original endpoint, but useSignedImageUrl fetches resolvedEndpoint. With thumbnail=true, a cached full-size URL can bypass IntersectionObserver and trigger an off-screen thumbnail request, while a cached thumbnail URL is ignored. Resolve the endpoint before initializing state and probe that same cache key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/clinical-dashboard/signed-image.tsx` around lines 56 - 69,
Compute resolvedEndpoint before initializing shouldLoad, then use
resolvedEndpoint with getCachedSignedUrl for cache gating. Ensure
useSignedImageUrl and the initial loading decision use the same resolved cache
key, including thumbnail query parameters.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Replace an existing w parameter instead of appending another one.

If endpoint already contains w, this produces values such as ?w=800&w=400. The route uses searchParams.get("w"), so it will honor the first value and the thumbnail will not necessarily be 400px. Use URLSearchParams.set("w", "400") when constructing the endpoint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/clinical-dashboard/signed-image.tsx` around lines 56 - 69,
Update the resolvedEndpoint construction in the signed-image component to parse
the endpoint query parameters and use URLSearchParams.set("w", "400") when
thumbnail is enabled, replacing any existing width value instead of appending a
duplicate. Preserve the original endpoint when thumbnail is disabled and retain
all other query parameters.


// Defer the request until the frame is near the viewport. A cached URL seeds
// `shouldLoad` synchronously, so already-fetched images skip the observer.
Expand DownExpand Up@@ -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>
)}
Expand Down
14 changes: 14 additions & 0 deletions worker/main.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose placeholders before generating them

Every retained ingestion image is now decoded and WebP-encoded, but withImageTableMetadata deletes the metadata in src/lib/document-detail.ts:136, buildVisualEvidence does not project placeholder_base64, and no repository caller passes placeholderBase64 to SignedImage. The UI therefore always uses the existing skeleton while ingestion pays the added native CPU and database-storage cost for every image; propagate the field through the relevant contracts and call sites, or remove the eager generation until it has a consumer.

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}`;
Expand DownExpand Up@@ -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,
Expand Down
Loading