From 2195ae1c1162b51ce963f92f8231a0a90f1d081f Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 13 Aug 2026 11:02:10 -0500 Subject: [PATCH 1/8] fix(commons): set changeSummary on constructed model versions The schema added ModelVersion.changeSummary but nothing in src ever referenced it, so every hand-built ModelVersionEntity was missing a required field and check-types failed across five files. --- .../model-additional-file/model-additional-file.service.spec.ts | 1 + .../modules/model-version-tag/model-version-tag.service.spec.ts | 1 + .../modules/model-version/domain/model-version.domain.spec.ts | 2 ++ .../src/modules/model-version/domain/model-version.domain.ts | 1 + .../src/modules/model-version/model-version.service.spec.ts | 1 + 5 files changed, 6 insertions(+) diff --git a/apps/modeling-commons-backend/src/modules/model-additional-file/model-additional-file.service.spec.ts b/apps/modeling-commons-backend/src/modules/model-additional-file/model-additional-file.service.spec.ts index 99066b4d..d64bfd62 100644 --- a/apps/modeling-commons-backend/src/modules/model-additional-file/model-additional-file.service.spec.ts +++ b/apps/modeling-commons-backend/src/modules/model-additional-file/model-additional-file.service.spec.ts @@ -18,6 +18,7 @@ function makeVersion(overrides: Partial = {}): ModelVersionE previewImageFileKey: null, netlogoFileKey: '2026/04/17/abcd-model.nlogox', netlogoVersion: null, + changeSummary: null, infoTab: null, createdAt: new Date(), finalizedAt: null, diff --git a/apps/modeling-commons-backend/src/modules/model-version-tag/model-version-tag.service.spec.ts b/apps/modeling-commons-backend/src/modules/model-version-tag/model-version-tag.service.spec.ts index 4ce7025c..7c40d31e 100644 --- a/apps/modeling-commons-backend/src/modules/model-version-tag/model-version-tag.service.spec.ts +++ b/apps/modeling-commons-backend/src/modules/model-version-tag/model-version-tag.service.spec.ts @@ -18,6 +18,7 @@ function makeVersion(overrides: Partial = {}): ModelVersionE previewImageFileKey: null, netlogoFileKey: 'files/key-1', netlogoVersion: null, + changeSummary: null, infoTab: null, createdAt: new Date(), finalizedAt: null, diff --git a/apps/modeling-commons-backend/src/modules/model-version/domain/model-version.domain.spec.ts b/apps/modeling-commons-backend/src/modules/model-version/domain/model-version.domain.spec.ts index 31362af7..4d4c61eb 100644 --- a/apps/modeling-commons-backend/src/modules/model-version/domain/model-version.domain.spec.ts +++ b/apps/modeling-commons-backend/src/modules/model-version/domain/model-version.domain.spec.ts @@ -30,6 +30,7 @@ describe('modelVersionDomain', () => { previewImageFileKey: null, netlogoFileKey: 'f1', netlogoVersion: null, + changeSummary: null, infoTab: null, createdAt: new Date(), finalizedAt: null, @@ -46,6 +47,7 @@ describe('modelVersionDomain', () => { previewImageFileKey: null, netlogoFileKey: 'f1', netlogoVersion: null, + changeSummary: null, infoTab: null, createdAt: new Date(), finalizedAt: new Date(), diff --git a/apps/modeling-commons-backend/src/modules/model-version/domain/model-version.domain.ts b/apps/modeling-commons-backend/src/modules/model-version/domain/model-version.domain.ts index 78869c50..2a4360b4 100644 --- a/apps/modeling-commons-backend/src/modules/model-version/domain/model-version.domain.ts +++ b/apps/modeling-commons-backend/src/modules/model-version/domain/model-version.domain.ts @@ -19,6 +19,7 @@ export default function modelVersionDomain() { previewImageFileKey: props.previewImageFileKey ?? null, netlogoFileKey: props.netlogoFileKey, netlogoVersion: null, + changeSummary: null, infoTab: null, createdAt: new Date(), finalizedAt: null, diff --git a/apps/modeling-commons-backend/src/modules/model-version/model-version.service.spec.ts b/apps/modeling-commons-backend/src/modules/model-version/model-version.service.spec.ts index 3b1fb6b2..4b67d226 100644 --- a/apps/modeling-commons-backend/src/modules/model-version/model-version.service.spec.ts +++ b/apps/modeling-commons-backend/src/modules/model-version/model-version.service.spec.ts @@ -20,6 +20,7 @@ function makeVersion(overrides: Partial = {}): ModelVersionE previewImageFileKey: null, netlogoFileKey: '2026/04/17/abcd-model.nlogox', netlogoVersion: null, + changeSummary: null, infoTab: null, createdAt: new Date(), finalizedAt: null, From 79f36d729082ce309514113c8bdaf4703ae7143d Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 13 Aug 2026 11:38:37 -0500 Subject: [PATCH 2/8] fix(commons): page tag search by page instead of offset The tag search sent an `offset` query param, but the API only accepts `limit` and `page`. Fastify dropped the unknown param and defaulted page to 0, so every "load more" refetched the first 20 tags and the tag select menu could never reach the rest. --- .../app/composables/tag/useTags.ts | 2 +- .../tests/nuxt/composables/useTags.test.ts | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 apps/modeling-commons-frontend/tests/nuxt/composables/useTags.test.ts diff --git a/apps/modeling-commons-frontend/app/composables/tag/useTags.ts b/apps/modeling-commons-frontend/app/composables/tag/useTags.ts index 8d6ede6c..ea714c8b 100644 --- a/apps/modeling-commons-frontend/app/composables/tag/useTags.ts +++ b/apps/modeling-commons-frontend/app/composables/tag/useTags.ts @@ -15,7 +15,7 @@ export default function useTags() { count, } = useApiPagination(key, async (page: number) => { const { data, error } = await GET("/api/v1/tags", { - params: { query: { limit: 20, offset: (page - 1) * 20, q: debouncedQuery.value } }, + params: { query: { limit: 20, page, q: debouncedQuery.value } }, }); const parsed = handleApiError(data, error, "fetching tags"); diff --git a/apps/modeling-commons-frontend/tests/nuxt/composables/useTags.test.ts b/apps/modeling-commons-frontend/tests/nuxt/composables/useTags.test.ts new file mode 100644 index 00000000..1f5812c9 --- /dev/null +++ b/apps/modeling-commons-frontend/tests/nuxt/composables/useTags.test.ts @@ -0,0 +1,38 @@ +import { mockNuxtImport } from "@nuxt/test-utils/runtime"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import useTags from "~/composables/tag/useTags"; +import { apiResult, makeApiClientMock } from "~~/tests/helpers/mockApi"; + +const { apiState } = vi.hoisted(() => ({ + apiState: { current: null as ReturnType | null }, +})); + +mockNuxtImport("useApi", () => { + return () => apiState.current!.client; +}); + +beforeEach(() => { + vi.resetAllMocks(); + apiState.current = makeApiClientMock(); +}); + +function lastQuery(path: string) { + const call = apiState.current!.GET.mock.calls.findLast(([called]) => called === path); + expect(call, `no GET call for ${path}`).toBeDefined(); + return call![1]?.params?.query as Record; +} + +describe("useTags", () => { + it("paginates with page, not an offset the API ignores", async () => { + apiState.current!.GET.mockResolvedValue( + apiResult.ok({ count: 0, limit: 20, page: 0, data: [] }), + ); + + useTags(); + await vi.waitFor(() => expect(apiState.current!.GET).toHaveBeenCalled()); + + const query = lastQuery("/api/v1/tags"); + expect(query.page).toBe(0); + expect(query).not.toHaveProperty("offset"); + }); +}); From 0663a37c76c6ebb012b885508c4e215420d822ed Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 13 Aug 2026 11:38:52 -0500 Subject: [PATCH 3/8] feat(commons): replace the tags coming-soon page with a real listing Lists tags by popularity with their model-version counts, and switches to prefix search while the search box has a value. Both listings page in on scroll. Prefix search carries no counts, so TagCard now takes an optional description. --- .../app/components/tag/TagCard.vue | 2 +- .../app/composables/tag/usePopularTags.ts | 23 ++++ .../app/pages/tags/index.vue | 107 ++++++++++++++++-- .../tests/e2e/smoke.test.ts | 1 + .../tests/nuxt/composables/useTags.test.ts | 16 +++ 5 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 apps/modeling-commons-frontend/app/composables/tag/usePopularTags.ts diff --git a/apps/modeling-commons-frontend/app/components/tag/TagCard.vue b/apps/modeling-commons-frontend/app/components/tag/TagCard.vue index 24b0081c..6808b59f 100644 --- a/apps/modeling-commons-frontend/app/components/tag/TagCard.vue +++ b/apps/modeling-commons-frontend/app/components/tag/TagCard.vue @@ -31,7 +31,7 @@ import { createTagPath } from "~/utils/formatters"; const props = defineProps<{ name: string; displayName?: string; - description: string; + description?: string; }>(); const label = computed(() => diff --git a/apps/modeling-commons-frontend/app/composables/tag/usePopularTags.ts b/apps/modeling-commons-frontend/app/composables/tag/usePopularTags.ts new file mode 100644 index 00000000..057b1380 --- /dev/null +++ b/apps/modeling-commons-frontend/app/composables/tag/usePopularTags.ts @@ -0,0 +1,23 @@ +export type PopularTag = ResponseSuccessData<"GET", "/api/v1/tags/popular">["data"][number]; + +export default function usePopularTags(limit = 24) { + const { GET } = useApi(); + + const { data, error, pending, loadNextPage, canLoadMore, count } = + useApiPagination(`popular-tags-${limit}`, async (page: number) => { + const { data, error } = await GET("/api/v1/tags/popular", { + params: { query: { limit, page } }, + }); + + return handleApiError(data, error, "fetching popular tags"); + }); + + return { + tags: data, + error, + pending, + loadNextPage, + canLoadMore, + count, + }; +} diff --git a/apps/modeling-commons-frontend/app/pages/tags/index.vue b/apps/modeling-commons-frontend/app/pages/tags/index.vue index faa2e7ea..5f64d5ef 100644 --- a/apps/modeling-commons-frontend/app/pages/tags/index.vue +++ b/apps/modeling-commons-frontend/app/pages/tags/index.vue @@ -1,9 +1,102 @@ + + diff --git a/apps/modeling-commons-frontend/tests/e2e/smoke.test.ts b/apps/modeling-commons-frontend/tests/e2e/smoke.test.ts index 5b370b67..bce94fd1 100644 --- a/apps/modeling-commons-frontend/tests/e2e/smoke.test.ts +++ b/apps/modeling-commons-frontend/tests/e2e/smoke.test.ts @@ -13,6 +13,7 @@ const PAGES: Array<{ path: string; identity: RegExp }> = [ { path: "/models", identity: /Explore Models/i }, { path: "/featured-models", identity: /Featured Models/i }, { path: "/new-models", identity: /New Models/i }, + { path: "/tags", identity: /Models by Tag/i }, { path: "/login", identity: /Log In|Welcome back/i }, { path: "/signup", identity: /Sign Up/i }, { path: "/reset-password", identity: /Reset password/i }, diff --git a/apps/modeling-commons-frontend/tests/nuxt/composables/useTags.test.ts b/apps/modeling-commons-frontend/tests/nuxt/composables/useTags.test.ts index 1f5812c9..c5764343 100644 --- a/apps/modeling-commons-frontend/tests/nuxt/composables/useTags.test.ts +++ b/apps/modeling-commons-frontend/tests/nuxt/composables/useTags.test.ts @@ -1,6 +1,7 @@ import { mockNuxtImport } from "@nuxt/test-utils/runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; import useTags from "~/composables/tag/useTags"; +import usePopularTags from "~/composables/tag/usePopularTags"; import { apiResult, makeApiClientMock } from "~~/tests/helpers/mockApi"; const { apiState } = vi.hoisted(() => ({ @@ -36,3 +37,18 @@ describe("useTags", () => { expect(query).not.toHaveProperty("offset"); }); }); + +describe("usePopularTags", () => { + it("requests /api/v1/tags/popular with the configured limit and page", async () => { + apiState.current!.GET.mockResolvedValue( + apiResult.ok({ count: 0, limit: 24, page: 0, data: [] }), + ); + + usePopularTags(24); + await vi.waitFor(() => expect(apiState.current!.GET).toHaveBeenCalled()); + + const query = lastQuery("/api/v1/tags/popular"); + expect(query.limit).toBe(24); + expect(query.page).toBe(0); + }); +}); From 7b2684a0bc2c93c35d0c993dca8ce247888a4110 Mon Sep 17 00:00:00 2001 From: Omar Ibrahim Date: Thu, 13 Aug 2026 11:38:57 -0500 Subject: [PATCH 4/8] perf(commons): serve the home feed from one cached endpoint The home page issued six per-request API calls, all of them public and identical for every visitor. They now come from a single `/_data/home` handler cached for ten minutes with stale-while-revalidate, so the page costs one internal call and the backend sees one set of queries per TTL rather than one per visit. Nitro's cached handler strips every header but the declared `varies` list, so the shared entry is built without a session and cannot leak one. --- .../app/pages/index.vue | 121 ++++-------------- .../server/routes/_data/home.get.ts | 48 +++++++ apps/modeling-commons-frontend/shared/home.ts | 71 ++++++++++ 3 files changed, 145 insertions(+), 95 deletions(-) create mode 100644 apps/modeling-commons-frontend/server/routes/_data/home.get.ts create mode 100644 apps/modeling-commons-frontend/shared/home.ts diff --git a/apps/modeling-commons-frontend/app/pages/index.vue b/apps/modeling-commons-frontend/app/pages/index.vue index 65df5f00..e79f4e9a 100644 --- a/apps/modeling-commons-frontend/app/pages/index.vue +++ b/apps/modeling-commons-frontend/app/pages/index.vue @@ -124,24 +124,19 @@

(in the past 2 weeks)

-
+
See all tags
-
- -
-
- -
@@ -159,18 +154,14 @@