feat: migrate health score displays to v2 and implement Health Score v2 content spec IN-1212 - #2054
feat: migrate health score displays to v2 and implement Health Score v2 content spec IN-1212#2054gaspergrom wants to merge 12 commits into
Conversation
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Migrates health-score read paths and UI surfaces from v1 calculations to Tinybird’s v2 fields while preserving v1 infrastructure.
Changes:
- Adds v2 score types, configuration, API route, and query integration.
- Updates collection, project overview, public API, and badge displays.
- Retains v1 data paths for follow-up removal.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
frontend/types/project.ts |
Adds v2 project insight fields. |
frontend/types/overview/responses.types.ts |
Defines the v2 overview response. |
frontend/server/api/project/[slug]/overview/health-score-v2.get.ts |
Exposes v2 overview data. |
frontend/server/api/badge/health-score.ts |
Sources badge labels from v2 data. |
frontend/config/trust-score.ts |
Adds v2 label and badge configuration. |
frontend/app/components/shared/types/tanstack.ts |
Adds the v2 query key. |
frontend/app/components/shared/components/health-score.vue |
Renames OSI score tiers. |
frontend/app/components/modules/project/views/overview.vue |
Integrates the new v2 score card. |
frontend/app/components/modules/project/services/overview.api.service.ts |
Adds the v2 overview query. |
frontend/app/components/modules/project/components/overview/trust-score-v2.vue |
Implements the v2 score card. |
frontend/app/components/modules/collection/components/details/health-score-details.vue |
Rebuilds collection score details. |
frontend/app/components/modules/collection/components/details/collection-project-item.vue |
Uses v2 collection score fields. |
frontend/app/components/modules/collection/components/details/collection-health-score-pill.vue |
Supports backend-provided v2 labels. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…er IN-1212 Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (4)
frontend/app/components/modules/project/views/overview.vue:17
- The v2 response includes
lifecycleLabel, but this view neither passes it to the new card nor renders it there. As a result, the project overview omits the lifecycle row promised by this migration even though the server already supplies the data. Pass the value through and display it intrust-score-v2.vue.
:health-label="healthScoreV2Data?.healthLabel ?? null"
frontend/server/api/badge/health-score.ts:19
healthLabelis nullable, and the other new UI treats a null v2 score/label as unavailable. Here, passing null intogetHealthScoreV2Configfalls back tocritical, so projects without enough data publish a misleading “Critical” badge. Handle the unavailable case explicitly before selecting the label/color (for example, return an unavailable badge or an appropriate HTTP response).
const config = getHealthScoreV2Config(res.data[0].healthLabel);
frontend/app/components/modules/project/views/overview.vue:49
- The v2 query is keyed only by project slug, so selecting a repository no longer hides or changes the displayed score. The prior card deliberately suppressed the project aggregate when
selectedReposValueswas non-empty; now the UI presents that aggregate as though it reflected the active repository selection. Keep the prior selected-repository treatment, or explicitly identify this as a project-wide score when a repository is selected.
const params = computed(() => ({
projectSlug: route.params.slug as string,
}));
frontend/app/components/modules/project/views/overview.vue:15
- This unconditional replacement drops the archived-project/all-repositories behavior from the previous card. The project store explicitly says archived projects/repositories are excluded from Health Score, but this component no longer reads
isArchived; archived scopes now show either a stale score or the generic “not enough data” message instead of the archived empty state. Preserve the archived guard and explanatory empty state around the v2 card.
<lfx-project-trust-score-v2
…iew components IN-1212 Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (7)
frontend/app/components/modules/project/views/overview.vue:18
lifecycleLabelis returned by the new endpoint but is never passed to or rendered by this card. The PR describes the overview card as showing score, label, and lifecycle, so the current implementation omits a required v2 value.
<lfx-project-trust-score-v2
:health-score-v2="healthScoreV2Data?.healthScoreV2 ?? null"
:health-label="healthScoreV2Data?.healthLabel ?? null"
:status="healthScoreV2Status"
frontend/app/components/modules/collection/components/details/collection-project-item.vue:75
- The collection health-score popover is removed here rather than migrated:
health-score-details.vueis deleted and this is now a bare pill. That makes the overall-score/lifecycle details described in the PR inaccessible on the collection page. Restore the popover and back it with the v2 details UI.
<lfx-collection-health-score-pill
v-else
:score="project.healthScoreV2 ?? 0"
:health-label="project.healthLabel"
/>
frontend/app/components/modules/project/components/overview/trust-score-v2.vue:12
- Archived projects and all-archived repository selections no longer get the archive empty state; this component renders a score or the generic no-data message instead. The store explicitly states that archived projects/repositories are excluded from Health Score (
project.store.ts:163-175), and the replaced component enforced that state. Restore the archived-state branch before rendering the score.
<lfx-skeleton-state
v-if="status === 'pending' || !isEmpty"
frontend/app/components/modules/project/views/overview.vue:49
- Repository selection is dropped from the request here, while the repository switch remains active on the overview page. Selecting a repository therefore still shows the project-wide v2 score as though it reflected the selected repository; the previous card intentionally hid the aggregate and prompted users to select all repositories. Preserve that state handling, or clearly disable/label the score when a repository filter is active.
const params = computed(() => ({
projectSlug: route.params.slug as string,
}));
frontend/config/trust-score.ts:84
- A missing or unrecognized
healthLabelis classified asCritical. Because the field is nullable and the collection pill explicitly handles sparseproject_insightsrows where the score exists but the label does not, both the overview and badge can falsely show a critical rating. Represent this as unavailable or derive a fallback fromhealthScoreV2instead.
export const getHealthScoreV2Config = (label: string | null): HealthScoreV2Config => {
if (label && healthScoreV2Config[label]) {
return healthScoreV2Config[label];
}
return healthScoreV2Config.critical;
frontend/app/components/shared/components/health-score.vue:35
- This component still receives a v1-derived score, so renaming its 40–59 v1 tier to the v2 term
Faircan report a label that differs from the project's actual v2healthLabel. Keep the v1 tier name until this surface is wired to v2 data.
Fair
frontend/app/components/shared/components/health-score.vue:41
- This component still receives a v1-derived score, so renaming its 20–39 v1 tier to the v2 term
Concerningcan report a label that differs from the project's actual v2healthLabel. Keep the v1 tier name until this surface is wired to v2 data.
Concerning
…N-1212 Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (5)
frontend/app/components/modules/project/views/overview.vue:18
- The PR description says the new overview card includes lifecycle, and the new endpoint returns
lifecycleLabel, but this invocation drops that field andtrust-score-v2.vuehas no lifecycle prop or row. Pass and renderhealthScoreV2Data.lifecycleLabel, or update the declared scope if lifecycle is intentionally deferred.
<lfx-project-trust-score-v2
:health-score-v2="healthScoreV2Data?.healthScoreV2 ?? null"
:health-label="healthScoreV2Data?.healthLabel ?? null"
:status="healthScoreV2Status"
frontend/app/components/modules/project/views/overview.vue:45
- The new card drops the archived-state guard that the previous overview card enforced.
useProjectStore()still defines project/all-repositories archival as excluding Health Score, but this view now always renders the score card, so archived contexts can show a stale project score (or a generic “not enough data” message) instead of the archive empty state. Restore theisArchived,emptyStateTitle, andemptyStateDescriptionhandling before rendering the v2 score.
const { hasSelectedArchivedRepos } = storeToRefs(useProjectStore());
frontend/app/components/modules/project/views/overview.vue:49
- This query is now keyed only by project slug even though this overview is also mounted by the repository and repository-group routes. In those contexts the card therefore displays the project-wide v2 score as if it applied to the selected repository/group; the previous implementation explicitly suppressed the aggregate score when repositories were selected. Preserve that guard (or provide a repository-scoped v2 endpoint) so filtered views do not present the wrong scope.
const params = computed(() => ({
projectSlug: route.params.slug as string,
}));
frontend/app/components/shared/components/health-score.vue:35
- This caller still supplies a v1-derived score and has no authoritative v2
healthLabel, so changing the 40–59 bucket to “Fair” can claim a v2 category that the backend did not return. Keep the v1 terminology until the OSI endpoints expose v2 data; visual consistency should not override score semantics.
Fair
frontend/app/components/shared/components/health-score.vue:41
- This is the same semantic mismatch for the 20–39 bucket: the UI now presents the v2 label “Concerning” while the value remains v1-derived. Retain the v1 label until these list callers are migrated to the actual v2 score/label.
Concerning
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (3)
frontend/app/components/modules/project/views/overview.vue:45
- Dropping the archived-state refs removes the established Health Score exclusion behavior. Fully archived selections and archived projects now render the project-wide v2 score (or a generic no-data message) instead of the archived empty state, even though
project.store.ts:161-175defines archived projects/repositories as excluded from Health Score. PreserveisArchivedand pass the existing empty-state title/description into the v2 card.
const { hasSelectedArchivedRepos } = storeToRefs(useProjectStore());
frontend/server/api/project/[slug]/overview/health-score-v2.get.ts:21
- The new endpoint is described as returning all five v2 fields, but this destructuring drops
lifecycleLabel,impactScore, andimpactLabel. Consumers therefore cannot receive the documented response. Return all five fields and extendHealthScoreV2Results, or update the PR/API contract if the two-field response is intentional.
const { healthScoreV2, healthLabel } = res.data[0];
return { healthScoreV2, healthLabel };
frontend/app/components/modules/project/components/overview/trust-score-v2.vue:79
- The PR describes this card as including a lifecycle row, but the component accepts and renders only the health score and health label. As implemented,
lifecycleLabelcan never reach this UI. Pass it through the route/service/view and render the lifecycle state, or correct the PR description if this is intentionally out of scope.
const props = defineProps<{
healthScoreV2: number | null;
healthLabel: string | null;
status: AsyncDataRequestStatus;
}>();
…olor IN-1212 Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (4)
frontend/app/components/modules/project/components/overview/trust-score-v2.vue:72
- The share badge component switches between the project Health Score badge and the repository active-contributors badge via its optional
isRepoSelectedprop. Omitting it makes the prop default to false, so a repository-filtered overview now generates the project-level badge instead of the repository badge.
<lfx-project-trust-score-share-badge />
frontend/app/components/modules/project/components/overview/trust-score-v2.vue:114
- The response contract allows a non-null score with a null label. In that state
isEmptyis false, but this rendersUnavailable <score>/100with a negative dot; the collection pill explicitly falls back to score-derived v2 labeling for the same sparse-label case. Derive both the label and color from the score when the label is absent, or treat the pair consistently as unavailable.
const scoreLabel = computed(() => getHealthScoreV2Config(props.healthLabel).label);
const scoreColorClass = computed(() => {
const label = props.healthLabel;
if (label === 'excellent' || label === 'healthy') return 'bg-positive-500';
if (label === 'fair' || label === 'concerning') return 'bg-health-concerning';
return 'bg-negative-500';
frontend/app/components/modules/project/components/overview/trust-score-v2.vue:69
- This guard no longer excludes archived scopes, so the Health Score share badge is displayed alongside the archived empty state even though archived repositories/projects are excluded from Health Score. Keep the previous
!isArchivedguard.
v-if="!isEmpty && status === 'success'"
frontend/app/components/modules/project/components/overview/trust-score-v2.vue:12
- This route fetches only by project slug, so the returned score is always the whole-project aggregate. The new condition still renders that value when a repository filter is active (and when the selected scope is archived), even though the notice says to select all repositories and archived repositories are excluded from Health Score. Restore the previous scope guards so an aggregate score is not presented as belonging to the current filtered/archived view.
This issue also appears in the following locations of the same file:
- line 69
- line 72
- line 108
<lfx-skeleton-state
v-if="status === 'pending' || !isEmpty"
…ge correctly IN-1212 Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.
Suppressed comments (2)
frontend/app/components/modules/project/components/overview/trust-score-v2.vue:13
- The aggregate project score is still rendered when a repository is selected. This query never includes repository parameters, so users see the project-wide value directly above the instruction to select “All repositories”; the prior implementation suppressed that value in repo scope. Gate the score display on
!isRepoSelected.
v-if="status === 'pending' || !isEmpty"
frontend/app/components/shared/components/health-score.vue:33
- Renaming this tier to the v2 “Fair” label without changing its variation leaves it blue, while every other v2 mapping renders both
fairandconcerningwith the warning/amber color (trust-score-v2.vue:119,collection-health-score-pill.vue:61, andtrust-score.ts:75-76). Use the warning variation so the same v2 label does not change meaning across surfaces.
Fair
…IN-1212 Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
frontend/app/components/modules/project/views/overview.vue:20
- The current PR description explicitly limits this card to
healthScoreV2/healthLabeland says the Lifecycle + Health + Impact redesign is follow-up work. Passing these fields—and adding the two breakdown cards below—reintroduces the broader redesign that the previous review response said was removed. Either remove this out-of-scope UI/data path or update the acceptance criteria and split/re-review the expanded feature.
:impact-score="healthScoreV2Data?.impactScore ?? null"
:impact-label="healthScoreV2Data?.impactLabel ?? null"
:lifecycle-label="healthScoreV2Data?.lifecycleLabel ?? null"
frontend/app/components/modules/project/components/overview/trust-score-v2.vue:9
fetchHealthScoreV2sends only the project slug, so this content is always the all-repositories aggregate. This condition still renders that score, impact, lifecycle, and the breakdown cards when a repository is selected, directly above a notice saying the aggregate requires “All repositories.” Gate all aggregate sections on no repository selection, or add a genuinely repository-scoped backend query.
v-if="status === 'pending' || !isEmpty"
frontend/server/api/project/[slug]/overview/health-score-v2.get.ts:28
- These three fields are not present in
project_insights_copy_dsand are not selected by the upstreamproject_insights.pipe; they exist only in the repo-level health-score pipeline. They therefore resolve toundefined, so every project health-breakdown category displays “No data.” Remove this breakdown or add and deploy an actual project-level rollup before consuming it.
maintainerHealthScoreV2,
securitySupplyChainScoreV2,
developmentActivityScoreV2,
frontend/app/components/modules/collection/components/details/collection-health-score-pill.vue:48
- The fallback now derives a v2 label from a v2 score, but it retains v1 cutoffs (80/60/40/20). The upstream v2 bands are 85/70/50/30, so sparse rows are misclassified (for example, 80 becomes Excellent instead of Healthy), and the dot cutoffs disagree too.
// Prefers the API's real healthLabel (v2) when present; falls back to deriving the label from
// the score for sparse rows where the pipe didn't return a label.
const healthScoreLabel = computed(() => {
if (props.healthLabel) {
return props.healthLabel.charAt(0).toUpperCase() + props.healthLabel.slice(1);
frontend/app/components/shared/components/health-score.vue:35
- This component still receives a v1 score and applies v1 thresholds, so renaming the buckets does not produce a v2 classification. The v2 cutoffs differ (85/70/50/30), meaning an OSI item can display a v2 term that its actual v2 backend label would not have. Keep the v1 terminology until these callers receive v2 data.
Fair
frontend/app/components/modules/project/views/overview.vue:44
- This condition is true when the health query is in
error, and the independent impact query's status is discarded entirely. A health failure produces an empty card, while an impact 404/500 leaves a header-only breakdown with no error or empty state. Track both query statuses and render the card only for success, with an explicit pending/error state if the breakdown remains in scope.
v-if="healthScoreV2Status !== 'pending' && !isArchived"
…eakdown styling IN-1212 Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (5)
frontend/app/components/modules/project/views/overview.vue:31
- This card is mounted after a failed or empty health-score response, but
health-breakdown-section.vuerenders nothing whenhealthScoreV2is null. That leaves an empty padded card below the main error/empty state. Gate the wrapper on an available score (or render an explicit state inside the section).
v-if="healthScoreV2Status !== 'pending' && !isArchived"
frontend/app/components/modules/project/components/overview/impact-breakdown-section.vue:79
- The breakdown endpoint is independent of the overall Osprey
impactScore, so it can contain downloads/dependent metrics when that summary score is null. KeyingisEmptytoimpactScorehides all available rows and also prevents the per-field “No data” states from rendering. Base visibility on the breakdown response/status and keep the summary chip optional.
const isEmpty = computed(() => props.impactScore === null);
frontend/app/components/modules/project/components/overview/trust-score-v2.vue:142
isEmptysuppresses the entire three-column layout whenever onlyhealthScoreV2is null. Impact and lifecycle are independently computed nullable fields, so valid values for those columns are lost for projects without an overall health score. Render each column’s unavailable state independently, or treat the card as empty only when all three values are absent.
const isEmpty = computed(() => props.healthScoreV2 === null);
frontend/types/project.ts:140
- This shared Tinybird type is also used for
project_repo_insightsinfrontend/server/api/collection/[slug]/project-repos.ts:111, but that pipe does not select these three category fields. Declaring them as always present makes that route’s runtime response disagree with its type. Use an endpoint-specific type, make the fields optional and normalize the overview response, or add them to the upstream pipe.
maintainerHealthScoreV2: number | null;
securitySupplyChainScoreV2: number | null;
developmentActivityScoreV2: number | null;
frontend/app/components/modules/project/views/overview.vue:44
- This gate watches only the health-score query. If that request finishes first, the Impact card mounts while its own data is still pending and shows no loading state; the inverse completion order can likewise leave the summary unavailable. Wait for both independently fetched responses before mounting the card.
v-if="healthScoreV2Status !== 'pending' && !isArchived"
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 41 changed files in this pull request and generated no new comments.
Suppressed comments (10)
frontend/config/health-breakdown-templates.ts:103
- Excellent projects can enter this branch and receive text saying a weakness is “keeping the score from Excellent,” contradicting the displayed Excellent label. Handle Excellent separately and reserve the gap wording for Healthy.
frontend/app/components/modules/project/views/overview.vue:44 - On a health-score request failure this card still renders with
impactScore=null, so it tells users the project publishes no tracked packages even though the actual state is a fetch error. Only render this dependent section after the health-score query succeeds.
v-if="healthScoreV2Status !== 'pending' && !isArchived"
frontend/app/components/modules/project/components/overview/impact-breakdown-section.vue:48
- A null centrality value is a no-data state, not a pending computation. The upstream rollout explicitly reports 0% coverage and says null should display “No data”; this condition instead always renders “Pending” and promises completion by a weekly job that does not populate centrality. Render the normal null state until the API exposes a distinct pending signal.
:description="getGraphCentralityDescription(data.centrality, data.centrality === null)"
:value="data.centrality"
:band="data.centralityBand"
:is-pending="data.centrality === null"
frontend/server/api/project/[slug]/overview/health-score-breakdown.get.ts:12
- This route depends on
project_insights_health_breakdown, but that endpoint is introduced by crowd.dev PR #4448, which is still open and behind its base branch. The deploy section currently says all backend dependencies are merged and only lists #4438/#4439. Please link #4448 and require it to merge before this route ships; a manually deployed but unmerged Tinybird contract can disappear on a later deployment.
frontend/app/components/modules/project/views/overview.vue:95 - The health-breakdown query discards its status/error. While it is pending—or permanently after failure—the component receives
signals=null;getCategoryDescriptionthen claims each scored category was dropped from the composite, and all signal rows disappear without an error. Pass the query status into the section and render loading/error states separately from genuine missing signal data.
const { data: healthBreakdownData, suspense: healthBreakdownSuspense } =
OVERVIEW_API_SERVICE.fetchHealthScoreBreakdown(params);
frontend/config/health-breakdown-templates.ts:29
- These descriptions assert signals the lifecycle calculation does not establish.
activeis the backend fallback and does not guarantee responsive maintainers, regular releases, or healthy triage;stableallows up to 49 open issues and does not test maintainer reachability. Use wording limited to the actual lifecycle classification criteria.
This issue also appears on line 99 of the same file.
frontend/config/health-breakdown-templates.ts:377
- The upstream contract documents all three vulnerability counts as null when no vulnerability rows exist. Coalescing those nulls to zero makes an unindexed project receive a positive “No open vulnerabilities” result. Return a no-data row when every count is null before evaluating severity totals.
frontend/app/components/modules/project/components/overview/health-breakdown/category-card.vue:8 - This introduces a raw
<button>even though the repository rule requireslfx-button/lfx-icon-buttonwhenever a uikit equivalent exists (.claude/rules/always-use-uikit.md:9-14). Please implement the selectable card through the uikit button API rather than bypassing the design-system component.
<button
type="button"
class="flex-1 min-w-0 flex flex-col gap-4 items-start p-4 rounded-md text-left transition-colors"
frontend/app/components/shared/components/health-score.vue:24
- This component still receives a v1 score, but now classifies it with v2 thresholds and labels. That guarantees mislabeled OSI rows—for example, a v1 score of 82 changes from Excellent to Healthy without the underlying calculation changing. Keep the v1 tiers until callers supply
healthScoreV2, or migrate the data source in the same change.
v-else-if="props.score >= 85"
variation="positive-solid"
>
Excellent
</lfx-tag>
frontend/app/components/modules/project/views/overview.vue:30
- On a health-score request failure this condition still mounts the breakdown card with all-null props; the child renders nothing, leaving an empty card below the real error state. Gate the dependent section on a successful health-score response.
This issue also appears on line 44 of the same file.
v-if="healthScoreV2Status !== 'pending' && !isArchived"
0974b22 to
549171d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 41 changed files in this pull request and generated 1 comment.
Suppressed comments (10)
frontend/app/components/modules/project/views/overview.vue:95
- The breakdown query's status is discarded. While it is loading—or after a 404/500—the section receives
signals = null, describes scored categories as “dropped,” and removes all signal rows, making an upstream failure look like genuine no-data. Preserve the query status and render loading/error states separately from a successful sparse response.
const { data: healthBreakdownData, suspense: healthBreakdownSuspense } =
OVERVIEW_API_SERVICE.fetchHealthScoreBreakdown(params);
frontend/app/components/modules/project/views/overview.vue:44
- On a primary v2 query failure this condition still renders the Impact section with
impactScore = null, so users are incorrectly told that the project publishes no tracked packages. Gate this card on a successful primary query rather than treating an API failure as valid empty data.
v-if="healthScoreV2Status !== 'pending' && !isArchived"
frontend/config/health-breakdown-templates.ts:173
- A category score reaching the success band does not prove there are zero CVEs or that every security practice is enabled; the nullable vulnerability counts and practice flags can contradict this unconditional sentence, and the signal rows below will then show both claims at once. Build this summary from the actual fields (and omit claims whose inputs are unavailable) rather than inferring all facts from the aggregate band.
frontend/config/health-breakdown-templates.ts:377 - All three vulnerability counts are nullable, and the upstream pipe documents them as NULL when there are no vulnerability rows, but nulls are converted to zero here. A project with no vulnerability data therefore renders a positive “No open vulnerabilities” row. Return
no-datawhenever the inputs needed to establish a clean result are missing.
frontend/config/health-breakdown-templates.ts:316 - This declares the whole responsiveness signal unavailable whenever the issue median is null, although the upstream response exposes
medianPrResponseSindependently and documents that either median can be NULL. When PR response data exists, the “No issue or PR response data” message is false. Incorporate the PR median as a fallback or combine both medians according to the content spec.
frontend/app/components/modules/project/views/overview.vue:30 - This card is also rendered when the primary v2 query is in
error, because every non-pending state passes this condition. In that case all props fall back tonull, leaving an empty Health breakdown card instead of preserving the Health-score error state. Only render it after a successful primary query.
This issue also appears in the following locations of the same file:
- line 44
- line 94
v-if="healthScoreV2Status !== 'pending' && !isArchived"
frontend/app/components/shared/components/health-score.vue:10
- This explicitly leaves the two OSI surfaces on a v1 score while applying v2 thresholds and labels. That can reclassify the same v1 value under a scale it was not computed for, and it contradicts the PR's claim that every health-score surface moved to
healthScoreV2. Wire these callers to v2 data in this PR, or retain the v1 labeling until that migration is delivered and scope the PR description accordingly.
Labels below use the v2 health score convention (excellent/healthy/fair/concerning/critical,
see collection-health-score-pill.vue) for consistency with the rest of the app. The score
itself is still v1 here — osi-list-projects.vue and osi-list-collections.vue (this component's
only callers) read healthScore from projects_list/collections_oss_index, neither of which
carries v2 fields. Wiring these lists to v2 needs a data-layer follow-up.
frontend/config/health-breakdown-templates.ts:103
excellentandhealthyshare this branch, so an Excellent project with more than one available category is told that a remaining gap is “keeping the score from Excellent.” Handle Excellent separately so the generated summary cannot contradict the displayed label.
This issue also appears in the following locations of the same file:
- line 173
- line 309
- line 374
frontend/app/components/modules/project/components/overview/health-breakdown/category-card.vue:8
- This introduces a raw
<button>even though the repository explicitly requireslfx-buttonorlfx-icon-buttonfor button controls (.claude/rules/always-use-uikit.md:9-14). Using the uikit control preserves the shared interaction, disabled, and accessibility behavior while allowing these card styles through attributes/slots.
<button
type="button"
class="flex-1 min-w-0 flex flex-col gap-4 items-start p-4 rounded-md text-left transition-colors"
frontend/types/overview/responses.types.ts:91
- This response type models the upstream
*Available,isGerrit, andisExcludedcolumns as booleans, but PR #4448 declares and returns them as nullable TinybirdUInt8values.fetchFromTinybirdperforms no conversion, so this API actually returns0 | 1 | nullwhile advertising booleans. Match the wire types (as the security flags already do) or normalize every flag in the route before returning it.
Migrates the health score card, breakdown tabs, share badge, collection health pill, and impact breakdown from the v1 backend to v2, and implements the Health Score v2 content spec end-to-end: - Real per-project generated copy (lifecycle description, category descriptions, signal-row descriptions, impact summary) replacing fixed/fabricated text that was previously identical across all projects, via a new config/health-breakdown-templates.ts module. - New health-breakdown-section.vue signal rows per category (Maintainer/Security/Development), driven by the new health-score-breakdown server route consuming crowd.dev's newly exposed sub-signal detail. - Impact breakdown badges, per-signal descriptions, and a Pending state for signals still being computed (e.g. graph centrality). - v1 -> v2 label/threshold migration (Excellent/Healthy/Fair/Concerning/ Critical @ 85/70/50/30) across trust-score.ts, collection health pill, and the GitHub badge endpoint, removing the dead v1 lfxTrustScore config and its orphaned score-display.vue consumer. - Archived-repos exclusion banner moved above the health-score card and reworded (design feedback), with a documentation tooltip on the Health Score label pending a real doc link. Depends on the crowd.dev CM-IN-1212 pipe chain (already deployed to production). Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
549171d to
2a325fd
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 41 changed files in this pull request and generated 1 comment.
Suppressed comments (11)
frontend/app/components/modules/project/views/overview.vue:44
- This condition renders the Impact breakdown after the overview request fails. The missing response is converted to
impactScore = null, so the card incorrectly claims the project publishes no tracked packages. Gate the card on a successful overview response.
<lfx-card
v-if="healthScoreV2Status !== 'pending' && !isArchived"
frontend/config/health-breakdown-templates.ts:28
activeandstabledo not guarantee this copy. The deployed lifecycle decision tree usesactiveas the fallback, whilestableallows up to 49 open issues and non-critical vulnerabilities; project rollup is also best-state-wins. These descriptions can therefore assert responsiveness, regular releases, or no actionable issues that the signals do not establish. Generate neutral/state-accurate copy or derive each claim from the corresponding raw signal.
frontend/config/health-breakdown-templates.ts:101- For an
excellentproject with non-identical category percentages, this branch says a category is “keeping the score from Excellent” even though the score is already Excellent. Handleexcellentseparately so the generated summary does not contradict the displayed label.
frontend/config/health-breakdown-templates.ts:314 - This checks only the issue median. The deployed v2 calculation averages PR and issue medians (falling back to whichever exists), and for non-Gerrit repositories with neither response stream the absence is a real negative score, not blocked data. As written, PR-only/Gerrit projects and unresponsive GitHub projects are both mislabeled “No data.” Mirror the upstream combined median and negative empty case.
frontend/app/components/modules/project/views/overview.vue:95 - The breakdown query status is discarded. While it is loading—or permanently after an error—
signalsisnull; the component then describes scored categories as unavailable/dropped and silently removes all signal rows. Pass the query status/error through and render a loading or error state before generating category copy.
const { data: healthBreakdownData, suspense: healthBreakdownSuspense } =
OVERVIEW_API_SERVICE.fetchHealthScoreBreakdown(params);
frontend/app/components/modules/project/views/overview.vue:30
- This condition also renders the Health breakdown after the overview request fails. Because all response props then default to
null, users see a low-coverage/unavailable-data explanation for what is actually a request error. Only render this data-dependent card after a successful overview response.
This issue also appears on line 43 of the same file.
<lfx-card
v-if="healthScoreV2Status !== 'pending' && !isArchived"
frontend/app/components/shared/components/health-score.vue:10
- This component explicitly still receives v1 scores, but the change applies v2 thresholds and labels to them. That produces labels that match neither the v1 contract nor actual v2 data, and contradicts the PR’s claim that every health-score surface was migrated. Feed these callers
healthScoreV2/healthLabel, or retain the v1 mapping until those list pipes are migrated.
Labels below use the v2 health score convention (excellent/healthy/fair/concerning/critical,
see collection-health-score-pill.vue) for consistency with the rest of the app. The score
itself is still v1 here — osi-list-projects.vue and osi-list-collections.vue (this component's
only callers) read healthScore from projects_list/collections_oss_index, neither of which
carries v2 fields. Wiring these lists to v2 needs a data-layer follow-up.
frontend/app/components/modules/project/components/overview/health-breakdown/category-card.vue:14
- This introduces a raw
<button>even though the repository’s UI rule requires the existinglfx-button/lfx-icon-buttoncomponents for buttons. Using the uikit component preserves shared interaction, focus, disabled, and styling behavior.
<button
type="button"
class="flex-1 min-w-0 flex flex-col gap-4 items-start p-4 rounded-md text-left transition-colors"
:class="
props.selected
? 'c-card !border-transparent !shadow-md !rounded-md'
: 'bg-transparent border border-transparent hover:bg-white/60'
"
@click="emit('select')"
frontend/app/components/modules/project/components/overview/trust-score/share-badge.vue:10
- When a repository is selected, this image is the active-contributors badge, but its alternative text still announces a Health Score badge. Bind the alt text to the same selection condition as
src.
<img
:src="props.isRepoSelected ? repoBadgeUrl : badgeUrl"
alt="Health Score Badge"
frontend/types/overview/responses.types.ts:91
- This interface does not match the raw Tinybird payload returned by the new route:
*Available,isGerrit, andisExcludedare nullable UInt8 values (0/1), not booleans, andbranchProtectionRequiredReviewsis an Int32 count that can exceed 1. Either normalize the route response or model the wire values accurately; otherwise consumers are type-checking against values that never arrive at runtime.
frontend/config/health-breakdown-templates.ts:172 - A successful aggregate category band does not imply zero vulnerabilities: the upstream score can remain above this threshold while
openCriticals,openHighs, oropenModeratesis nonzero. This unconditional sentence can display “No open CVEs” beside a real vulnerability count. Build that clause from the counts instead of the aggregate band.
| fetchHealthScoreV2(params: ComputedRef<{ projectSlug: string }>) { | ||
| const queryKey = computed(() => [TanstackKey.HEALTH_SCORE_V2, params.value.projectSlug]); | ||
| const queryFn: QueryFunction<HealthScoreV2Results> = async () => | ||
| await $fetch(`/api/project/${params.value.projectSlug}/overview/health-score-v2`); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 41 changed files in this pull request and generated 2 comments.
Suppressed comments (9)
frontend/config/health-breakdown-templates.ts:385
openModeratescontains only moderate-severity findings, but this sentence labels the count as “medium or low.” That overstates what the returned value represents.
frontend/app/components/modules/project/views/overview.vue:95- The breakdown query status is discarded. If this request fails while the score request succeeds,
signalsbecomes null and every scored category is rendered with an unavailable/dropped description, with no error shown. Preserve the query status and render an error state instead of treating transport failure as domain no-data.
const { data: healthBreakdownData, suspense: healthBreakdownSuspense } =
OVERVIEW_API_SERVICE.fetchHealthScoreBreakdown(params);
frontend/app/components/modules/project/components/overview/health-breakdown-section.vue:37
- This asserts a specific coverage reason whenever the overall score is null, but the PR’s known gap states the backend cannot distinguish low coverage from other unavailable states such as no indexed repositories. This will show fabricated diagnostics for those projects; use neutral unavailable copy until an explicit reason is returned.
<span>Health Score unavailable. All three categories have less than 40% signal coverage for this project.</span>
frontend/config/health-breakdown-templates.ts:231
- A null category score or missing breakdown response is not proof that the category was dropped for signal coverage, yet this path delegates to copy that states exactly that. The backend currently provides no reason code, so projects with no indexed repos—and transient request failures—receive a false explanation. Use neutral copy or add an explicit availability reason before claiming a category was dropped.
frontend/config/health-breakdown-templates.ts:314 - Checking only the issue median misclassifies Gerrit projects (which have no issue tracker by design) and projects with PR-only response data, even though the backend marks responsiveness available and scores the PR median. Compute the effective PR/issue response time using the backend’s Gerrit/coalescing rules; when an available non-Gerrit project has neither median, that is a negative signal rather than no-data.
frontend/app/components/modules/project/components/overview/impact-breakdown-section.vue:48 - A null centrality value is the API’s no-data state, not a computation-status signal; the upstream pipe explicitly preserves null for missing values and says consumers should render “no data.” Treating every null as Pending promises a weekly completion that may never occur. Render no-data unless the API adds an explicit pending status.
:description="getGraphCentralityDescription(data.centrality, data.centrality === null)"
:value="data.centrality"
:band="data.centralityBand"
:is-pending="data.centrality === null"
frontend/app/components/modules/project/components/overview/health-breakdown/category-card.vue:8
- This introduces a raw
<button>even though the repository’salways-use-uikit.mdrule requireslfx-buttonorlfx-icon-buttonwhenever a button is needed. Use the uikit button and preserve these card classes/slot contents so interaction styling and accessibility behavior remain centralized.
<button
type="button"
class="flex-1 min-w-0 flex flex-col gap-4 items-start p-4 rounded-md text-left transition-colors"
frontend/app/components/shared/components/health-score.vue:20
- These callers still pass the legacy v1
healthScorefromprojects_listandcollections_oss_index, as the new comment acknowledges. Applying v2 thresholds to a v1 score does not migrate this surface and can display a label that disagrees with the project’s real v2 label. Wire v2 fields through these list APIs, or retain the v1 bands until that migration is available.
v-else-if="props.score >= 85"
frontend/app/components/modules/collection/components/details/collection-metrics-row.vue:109
avgHealthScoreis still computed upstream by averaging the legacyhealthScore, so applying the v2 85/70/50/30 bands relabels a v1 aggregate rather than migrating the metric. This can disagree with the v2 labels shown in the project rows. The aggregate pipe must averagehealthScoreV2(with its null semantics), or this UI must keep the v1 bands.
if (score >= 85) return 'Excellent';
if (score >= 70) return 'Healthy';
if (score >= 50) return 'Fair';
if (score >= 30) return 'Concerning';
| const params = computed(() => ({ | ||
| projectSlug: route.params.slug as string, | ||
| repos: selectedReposValues.value, | ||
| })); |
| export const getOpenVulnRow = (signals: HealthBreakdownResults): SignalRow => { | ||
| const criticals = signals.openCriticals ?? 0; | ||
| const highs = signals.openHighs ?? 0; | ||
| const moderates = signals.openModerates ?? 0; |
Summary
health_score_overview, category-average calculation) to v2 (project_insights,healthScoreV2/healthLabel), and implements the Health Score v2 Content Spec end-to-end (spec:attachments/IN-1212/content-spec.md).crowd.devPR #4448 (already deployed to production) exposes a new sub-signal pipe (project_insights_health_breakdown) that this PR's newhealth-score-breakdown.get.tsroute consumes. An earlier revision of this PR omitted the per-signal rows because that data didn't exist yet — it does now, and the rows are live.config/health-breakdown-templates.tsmodule, following the bracket-pattern templates in the content spec. None of it is fixed text repeated across projects.lfxTrustScorearray,getHealthScoreConfig, theTrustScoreConfiginterface, the orphanedscore-display.vuecomponent, and the old 4-tabscore-tabs.vue/score-details/breakdown UI that the new health/impact breakdown sections replace.Changes
frontend/config/health-breakdown-templates.tsfrontend/config/trust-score.tslfxTrustScore/getHealthScoreConfig/TrustScoreConfig. Adds v2 label/threshold config, impact label display, lifecycle label config.frontend/server/api/project/[slug]/overview/health-score-v2.get.tsproject_insights, returns health/impact/lifecycle scores and labels for the overview card.frontend/server/api/project/[slug]/overview/health-score-breakdown.get.tsproject_insights_health_breakdown— the sub-signal detail this PR's signal rows render.frontend/server/api/project/[slug]/overview/health-score-impact.get.tsproject_insights_impact_breakdownfor the 4 impact metric rows.frontend/server/api/badge/health-score.tshealth_score_overview/overallScoretoproject_insights/healthLabel, and now derives the badge color from real v2 design tokens instead of a hardcoded value.frontend/app/components/modules/project/components/overview/trust-score-v2.vuefrontend/app/components/modules/project/components/overview/trust-score/health-score-ring.vuelfx-chartgauge config pattern already live insecurity-score.vue.frontend/app/components/modules/project/components/overview/health-breakdown-section.vuefrontend/app/components/modules/project/components/overview/health-breakdown/category-card.vuefrontend/app/components/modules/project/components/overview/impact-breakdown-section.vuestatus-driven error state.frontend/app/components/modules/project/components/overview/impact-breakdown/metric-row.vuefrontend/app/components/modules/project/views/overview.vuefrontend/app/components/modules/project/services/overview.api.service.tsfetchHealthScoreV2,fetchHealthScoreBreakdown,fetchHealthScoreImpactBreakdownquery functions.frontend/app/components/modules/collection/components/details/collection-health-score-pill.vuehealthLabelwhen present (v2), with v1-threshold fallback for sparse rows. Fixes the Fair/Concerning color collision.frontend/app/components/modules/collection/components/details/collection-project-item.vueisHealthScoreUnavailablenow checkshealthScoreV2 == nullinstead of any of the 4 v1 category scores.frontend/app/components/modules/collection/components/details/collection-metrics-row.vuefrontend/app/components/modules/collection/components/details/health-score-details.vuefrontend/app/components/modules/project/components/overview/trust-score.vue,score-tabs.vue,score-details/*trust-score-v2.vue+ the new breakdown sections.frontend/app/components/modules/project/components/overview/trust-score/score-display.vuefrontend/app/components/modules/project/components/overview/trust-score/share-badge.vuefrontend/app/components/shared/components/repos-exclusion-footer.vuefrontend/app/components/shared/components/health-score.vuefrontend/app/components/uikit/benchmarks/benchmark-icon.vue,benchmarks.scssno-data/circle variant used by the new signal rows.frontend/app/components/uikit/chart/configs/gauge.chart.ts,ChartTypes.tsgraphOnlygauge mode used byhealth-score-ring.vue.frontend/app/components/uikit/tag/*dashedtag type used for the impact breakdown's "Pending" state.frontend/app/config/styles/colors.ts,frontend/tailwind.config.jshealth-faircolor token (Tailwind safelist) needed by the new Fair threshold band.frontend/types/overview/responses.types.tsHealthScoreV2Results,ImpactBreakdownResults,HealthBreakdownResultstypes.frontend/types/project.tshealthScoreV2,healthLabel,lifecycleLabel,impactScore,impactLabel, category scores) toProjectInsightsTinybirdandProjectInsights, additive alongside existing v1 fields.JIRA
IN-1212 — Migrate health score displays to v2 backend + Health Score v2 Content Spec
Known gaps / follow-ups
Confirmed with the ticket owner as intentionally deferred, not blocking this PR:
crowd.devpipe only computes 4 (foundational/major/moderate/minor). This PR ships with the 4-band vocabulary the backend actually returns. Needs a spec-owner decision on whether to add a 5th band upstream or update the spec.trust-score-v2.vuebut is commented out pending a real doc URL from design.Deploy order
crowd.devPR #4448 — already deployed to production. Exposesproject_insights_health_breakdownand the other v2 sub-signal pipes this PR's routes consume.DB migrations
No DB migrations.
Test plan
pnpm test— 133/133 passingpnpm tsc-check— cleanpnpm lint:fix— cleancrowd-insights-qa— 7/7 acceptance criteria PASS (one badge-color drift found and fixed during QA)Checklist
git commit --signoff -Son every commit authored in this PR (all 10IN-1212commits are signed and GPG-verified)crowd.devPR #4448, above)Note on diff size: this PR is well over the 1000-line target. Two commits in the branch history (
82edccb1,f868049c) are unrelated fixes/chores from other authors pulled in frommain—git diff origin/main...HEADalready excludes them, so they don't add to the count above. The size is inherent to the scope: a full v1→v2 migration plus the Figma-driven breakdown redesign across 3 new sections, all consuming the same crowd.dev PR #4448 dependency. An earlier revision of this PR shipped only the score-card migration without the breakdown sections; product then confirmed the full redesign was in scope for this ticket, which is why this revision includes both.