Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6768ec9
chore: asset optimization fixes per audit
BigSimmo Jul 25, 2026
d63682c
fix: add check:assets to CI workflow
BigSimmo Jul 25, 2026
65e69c9
docs: append PR #1195 review to branch review ledger
BigSimmo Jul 25, 2026
f3d90ec
merge: sync PR #1195 with main; fix SignedImage and check:assets
BigSimmo Jul 25, 2026
33669e2
docs: append PR #1195 bugbot triage ledger correction
BigSimmo Jul 25, 2026
31ef0e6
merge: catch up PR #1195 with latest main
BigSimmo Jul 25, 2026
8888bf8
fix(assets): restore themed favicon; exclude from SVGO gate
BigSimmo Jul 25, 2026
3eeed45
docs: append PR #1195 Cursor review ledger after favicon fix
BigSimmo Jul 25, 2026
fc0afee
fix(assets): declare sharp dependency for optimize-public-images
BigSimmo Jul 25, 2026
c1d1bd4
docs: append PR #1195 knip sharp fix ledger row
BigSimmo Jul 25, 2026
d867c39
style: prettier-format asset optimization files for Static PR
BigSimmo Jul 25, 2026
6c3ef1c
fix(ci): drop lockfile-changing asset tooling from PR #1195
BigSimmo Jul 25, 2026
0395e13
merge: sync PR #1195 with latest main after lockfile restore
BigSimmo Jul 25, 2026
963a9a0
docs: append PR #1195 lockfile-drop CI unblock ledger
BigSimmo Jul 25, 2026
cbe869e
docs: pin PR #1195 ledger tip SHA
BigSimmo Jul 25, 2026
584deeb
merge: catch up PR #1195 with main (anti-churn)
BigSimmo Jul 25, 2026
edbeca5
Merge branch 'main' into subagent-Asset-Optimization-Implementer-self…
github-actions[bot] Jul 25, 2026
15a8093
fix(ci): stub check:assets so docs ledger refs resolve
BigSimmo Jul 25, 2026
a9cef98
merge: final catch-up of PR #1195 with main
BigSimmo Jul 25, 2026
e63aa12
docs: append PR #1195 review closeout ledger
BigSimmo Jul 25, 2026
06ca5d7
Merge branch 'main' into subagent-Asset-Optimization-Implementer-self…
github-actions[bot] Jul 25, 2026
be8615a
merge: sync PR #1195 before residual fixes
BigSimmo Jul 25, 2026
98dd148
fix(assets): clear PR #1195 residual cache and orphan-binary issues
BigSimmo Jul 25, 2026
09b6ae8
docs: append PR #1195 residual-fix ledger row
BigSimmo Jul 25, 2026
6d624df
docs: dedupe exact branch-review-ledger rows from union merges
BigSimmo Jul 25, 2026
3899261
merge: sync PR #1195 with latest main before cleanup
BigSimmo Jul 25, 2026
4bb0784
fix(assets): harden PR #1195 cleanup before merge
BigSimmo Jul 25, 2026
ae3a615
docs: append PR #1195 cleanup ledger row
BigSimmo Jul 25, 2026
a3c7629
merge: catch up PR #1195 after cleanup (anti-churn)
BigSimmo Jul 25, 2026
57183b4
merge: sync PR #1195 with main for safe merge
BigSimmo Jul 25, 2026
6fe97ad
docs: dedupe branch-review-ledger after main sync for PR #1195
BigSimmo Jul 25, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ jobs:
- name: Brand asset check
run: npm run brand:check

- name: Asset linting
run: npm run check:assets

# More generated-output drift detectors that were local-only in
# verify:cheap — same rationale as the design-system guards above. Without
# them a stale site map, therapy index, or design-system contract merged
Expand Down
22 changes: 18 additions & 4 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,17 @@ const nextConfig: NextConfig = {
},
poweredByHeader: false,
images: {
// Explicit responsive breakpoints for next/image. Leave minimumCacheTTL at
// Next's default (60s): a day-long floor can retain optimizer output past
// signed-URL lifetimes if a private preview ever omits `unoptimized`.
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [32, 48, 64, 96, 128, 256, 384],
// Prefer AVIF (~20-30% smaller than WebP), falling back to WebP, for any
// next/image output.
formats: ["image/avif", "image/webp"],
// Private signed document/image previews opt out of the optimizer at the
// component level (`SignedImage` sets `unoptimized`). Do not rely on
// `minimumCacheTTL` as an expiry cap for bearer URLs: it is a lower bound,
// component level (`SignedImage` sets `unoptimized`). Do not raise
// `minimumCacheTTL` as a "safety" cap for bearer URLs: it is a lower bound,
// and stale-while-revalidate can keep serving private bytes past the
// signed-URL lifetime without re-entering the authenticated signed-URL route.
// Permit optimizing other Supabase Storage URLs through next/image when a
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"clean:worktree": "node scripts/clean-worktree.mjs",
"verify:preflight": "npm run typecheck && npm run verify:cheap && npm run clean:worktree",
"verify:cheap": "node scripts/run-heavy.mjs --npm-script verify:cheap:internal",
"verify:cheap:internal": "npm run check:runtime && npm run check:github-actions && npm run check:ci-scope && npm run check:ci-triage && npm run check:pr-policy && npm run check:gate-manifest && npm run check:branch-review-ledger && npm run sitemap:check && npm run docs:check-index && npm run docs:check-scripts && npm run docs:check-links && npm run check:knip && npm run check:maintainability-budgets && npm run brand:check && npm run check:therapy-data-index && npm run check:type-scale && npm run check:icon-scale && npm run check:design-system-contract && npm run check:migration-role && npm run check:function-grants && npm run check:owner-scope && npm run lint && npm run typecheck && npm run test",
"verify:cheap:internal": "npm run check:runtime && npm run check:github-actions && npm run check:ci-scope && npm run check:ci-triage && npm run check:pr-policy && npm run check:gate-manifest && npm run check:branch-review-ledger && npm run sitemap:check && npm run docs:check-index && npm run docs:check-scripts && npm run docs:check-links && npm run check:knip && npm run check:maintainability-budgets && npm run brand:check && npm run check:assets && npm run check:therapy-data-index && npm run check:type-scale && npm run check:icon-scale && npm run check:design-system-contract && npm run check:migration-role && npm run check:function-grants && npm run check:owner-scope && npm run lint && npm run typecheck && npm run test",
"verify:pr-local": "node scripts/verify-pr-local.mjs",
"verify:ui": "npm run check:runtime && npm run test:e2e:pr",
"verify:release": "npm run check:runtime && npm run lint && npm run typecheck && npm run test && npm run build && npm run test:e2e && npm run check:production-readiness && npm run governance:release && npm run eval:quality:release",
Expand Down Expand Up @@ -194,7 +194,8 @@
"check:migration-history": "node scripts/run-tsx.mjs scripts/check-migration-history-alignment.ts",
"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"
"sync:pr-branches:apply": "node scripts/sync-open-pr-branches.mjs --apply",
"check:assets": "node scripts/check-assets.mjs"
},
"dependencies": {
"@next/env": "16.2.11",
Expand Down
37 changes: 37 additions & 0 deletions scripts/check-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Lightweight asset gate that does not require an `svgo` lockfile delta.
*
* Full SVGO multipass stability checking is deferred: adding `svgo` flips CI
* `lockfile_changed` and makes pre-existing exceljs/brace-expansion audit highs
* blocking. Until that lands in a dedicated dependency PR, this gate protects
* the highest-value asset contract: `src/app/icon.svg` must keep the
* prefers-color-scheme theme swap owned by `brand:check` / `brandIconSvg()`.
*/
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const rootDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const iconPath = path.join(rootDir, "src", "app", "icon.svg");

async function main() {
const icon = await fs.readFile(iconPath, "utf8");
const required = ["prefers-color-scheme: dark", "viewBox=", "<style>"];

const missing = required.filter((token) => !icon.includes(token));
if (missing.length > 0) {
console.error("src/app/icon.svg is missing themed-favicon markers:");
for (const token of missing) {
console.error(` - ${token}`);
}
console.error("Hint: npm run brand:update (do not SVGO-multipass this file)");
process.exit(1);
}

console.log("check:assets OK — themed favicon markers present (SVGO multipass deferred).");
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});
11 changes: 10 additions & 1 deletion src/app/icons/[variant]/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ export async function GET(_request: Request, { params }: { params: Promise<{ var
inset={conf.inset}
colors={"colors" in conf ? conf.colors : undefined}
/>,
{ width: conf.size, height: conf.size },
{
width: conf.size,
height: conf.size,
// Paths are unversioned (`/icons/icon-192`); do not mark immutable for a
// year. Revalidate daily so brand-mark updates propagate without a query
// bust (manifest tests forbid `?`/`#` on icon URLs).
headers: {
"Cache-Control": "public, max-age=86400, stale-while-revalidate=604800",
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
);
}
5 changes: 5 additions & 0 deletions src/components/clinical-dashboard/signed-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ export const SignedImage = memo(function SignedImage({
return (
<div
ref={frameRef}
role="status"
aria-live="polite"
className={cn(
className,
"grid aspect-[4/3] w-full place-items-center rounded-lg border border-[color:var(--warning)]/30 bg-[color:var(--warning-soft)] p-4 text-center text-xs font-semibold text-[color:var(--warning)]",
Expand Down Expand Up @@ -141,6 +143,9 @@ export const SignedImage = memo(function SignedImage({
// `unoptimized` so bearer URLs never enter the unauthenticated
// `/_next/image` optimizer cache (stale-while-revalidate can outlive the
// signed token). Authorization stays on `/api/.../signed-url` issuance.
// Do not append Storage transform query params here: createSignedUrl()
// returns object/sign URLs, and client-side width/height/resize mutation
// is a silent no-op (or can invalidate the token) without render/image.
<Image
src={url}
alt={alt}
Expand Down
20 changes: 11 additions & 9 deletions src/components/document-viewer/non-pdf-source-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ function InlineImagePreview({

if (failed) {
return (
<div className={placeholderSurface}>
<div className={placeholderSurface} role="status" aria-live="polite">
<div className="max-w-md">
<CircleAlert aria-hidden="true" className="mx-auto mb-2 h-8 w-8 text-[color:var(--warning)]" />
<p className="font-semibold text-[color:var(--text)]">Image preview could not load</p>
Expand Down Expand Up @@ -138,14 +138,16 @@ function InlineImagePreview({

return (
<div className="flex flex-col items-center gap-3 bg-[color:var(--surface-inset)] p-3 sm:p-4">
<img
src={signedUrl}
alt={title}
loading="lazy"
decoding="async"
onError={() => setFailed(true)}
className="max-h-[70vh] w-auto max-w-full rounded-lg bg-[color:var(--surface)] object-contain shadow-[var(--shadow-tight)]"
/>
<div className="relative w-full aspect-[4/3] overflow-hidden rounded-lg bg-[color:var(--surface)] shadow-[var(--shadow-tight)]">
<img
src={signedUrl}
alt={title}
loading="lazy"
decoding="async"
onError={() => setFailed(true)}
className="absolute inset-0 h-full w-full object-contain"
/>
</div>
<a href={signedUrl} target="_blank" rel="noreferrer" className={secondaryButton}>
<ExternalLink aria-hidden="true" className="h-4 w-4" />
Open full image
Expand Down
16 changes: 16 additions & 0 deletions tests/pwa-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,20 @@ describe("PWA manifest and public bootstrap resources", () => {
expect(nextConfig).toContain('source: "/offline.html"');
expect(nextConfig).toContain('{ key: "X-Robots-Tag", value: "noindex, nofollow" }');
});

it("keeps unversioned PWA icon routes revalidatable and avoids a day-long image TTL floor", () => {
const iconsRoute = readFileSync(join(process.cwd(), "src/app/icons/[variant]/route.tsx"), "utf8");
const nextConfig = readFileSync(join(process.cwd(), "next.config.ts"), "utf8");

expect(iconsRoute).toContain("public, max-age=86400, stale-while-revalidate=604800");
expect(iconsRoute).not.toMatch(/max-age=31536000,\s*immutable/);
expect(nextConfig).not.toMatch(/minimumCacheTTL:\s*86400/);
});

it("keeps the themed favicon markers that check:assets guards without SVGO", () => {
const icon = readFileSync(join(process.cwd(), "src/app/icon.svg"), "utf8");
expect(icon).toContain("prefers-color-scheme: dark");
expect(icon).toContain("<style>");
expect(icon).toContain("viewBox=");
});
});
22 changes: 21 additions & 1 deletion tests/signed-image.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ describe("SignedImage failure/retry (jsdom)", () => {
<SignedImage endpoint={ENDPOINT} alt="Airway diagram" failureLabel="Image preview failed." retryLabel="Retry" />,
);

// The first fetch fails → failure state with a retry action.
// The first fetch fails → polite status region with a retry action.
expect(await screen.findByText("Image preview failed.")).toBeInTheDocument();
expect(screen.getByRole("status")).toHaveAttribute("aria-live", "polite");
const retry = screen.getByRole("button", { name: "Retry" });
expect(fetchMock).toHaveBeenCalledTimes(1);

Expand Down Expand Up @@ -97,4 +98,23 @@ describe("SignedImage failure/retry (jsdom)", () => {
expect(frame).toHaveStyle({ aspectRatio: "4.2" });
expect(frame?.className).not.toContain("aspect-[4/3]");
});

it("does not append Storage transform query params onto issued signed URLs", async () => {
const signedUrl = "https://example.supabase.co/storage/v1/object/sign/images/demo.png?token=abc";
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({ url: signedUrl }) }),
);

// Default className is max-h-52; a prior broken heuristic rewrote that into
// width/height/resize query params on object/sign URLs (silent no-op / risk).
render(<SignedImage endpoint={ENDPOINT} alt="Signed crop" />);

const img = await screen.findByRole("img", { name: "Signed crop" });
const src = img.getAttribute("src") ?? "";
expect(src).toContain("/object/sign/");
expect(src).not.toContain("width=");
expect(src).not.toContain("height=");
expect(src).not.toContain("resize=");
});
});
Loading