Repository: ethproofs/ethproofs
Pull request #381: No zkvm metrics state
Author: login: pettinarip
Created: 2025-05-16T13:14:43Z | Merged: 2025-05-16T19:56:38Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added the ability to visually disable the LevelMeter component, including dimming and hiding value indicators when disabled.
  - UI elements such as charts and value markers now display a disabled state when data is unavailable.

- **Improvements**
  - Enhanced handling of missing or partial metric data throughout the app, ensuring UI components gracefully handle unavailable information.
  - Unified metric fetching for multiple items into a single, more efficient operation, improving performance and reliability.
  - Updated color handling in the Pizza component to support a fallback style for undefined severity levels.

- **Bug Fixes**
  - Resolved issues where undefined severity levels could cause errors or incorrect UI states by allowing optional severity values.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/components/LevelMeter.tsx b/components/LevelMeter.tsx
index 319424b5..a3742ebb 100644
--- a/components/LevelMeter.tsx
+++ b/components/LevelMeter.tsx
@@ -5,6 +5,7 @@ export type LevelMeterProps = React.ComponentProps<"div"> & {
   worstThreshold: number
   value: number
   unit: string
+  disabled?: boolean
 }
 const LevelMeter = ({
   className,
@@ -12,6 +13,7 @@ const LevelMeter = ({
   worstThreshold,
   value,
   unit,
+  disabled,
 }: LevelMeterProps) => {
   const valueXPosition = (() => {
     const THIRD = 100 / 3
@@ -45,7 +47,11 @@ const LevelMeter = ({
 
   return (
     <div
-      className={cn("relative grid w-full grid-cols-6 space-y-1", className)}
+      className={cn(
+        "relative grid w-full grid-cols-6 space-y-1",
+        disabled && "grayscale",
+        className
+      )}
     >
       <div
         data-label="colored-meter"
@@ -72,25 +78,27 @@ const LevelMeter = ({
         {unit}
       </div>
 
-      <div
-        data-label="current-value"
-        className="absolute z-10"
-        style={{ insetInlineStart: valueXPosition + "%" }}
-      >
-        <span className="absolute -translate-x-1/2 -translate-y-9 font-mono">
-          {value}
-          {unit}
-          <div className="relative flex w-full flex-col">
-            <div className="col-span-2 place-items-center">
-              <div className="absolute bottom-full start-1/2 size-1 -translate-x-1/2 rounded-full bg-body" />
+      {!disabled && (
+        <div
+          data-label="current-value"
+          className="absolute z-10"
+          style={{ insetInlineStart: valueXPosition + "%" }}
+        >
+          <span className="absolute -translate-x-1/2 -translate-y-9 font-mono">
+            {value}
+            {unit}
+            <div className="relative flex w-full flex-col">
+              <div className="col-span-2 place-items-center">
+                <div className="absolute bottom-full start-1/2 size-1 -translate-x-1/2 rounded-full bg-body" />
+              </div>
+              <div className="flex h-4 w-full">
+                <div className="flex-1 border-e border-body"></div>
+                <div className="flex-1" data-label="spacer" />
+              </div>
             </div>
-            <div className="flex h-4 w-full">
-              <div className="flex-1 border-e border-body"></div>
-              <div className="flex-1" data-label="spacer" />
-            </div>
-          </div>
-        </span>
-      </div>
+          </span>
+        </div>
+      )}
     </div>
   )
 }
diff --git a/components/Pizza.tsx b/components/Pizza.tsx
index 97f3133e..efc39e81 100644
--- a/components/Pizza.tsx
+++ b/components/Pizza.tsx
@@ -14,7 +14,7 @@ type PizzaProps = React.ComponentProps<"div"> & {
  * Each slice can have different levels and hover effects.
  *
  * @param {PizzaProps} props - The properties for the Pizza component.
- * @param {Array<{ level: SeverityLevel }>} props.slices - An array of slice objects, each containing a `level` property
+ * @param {Array<{Slices}>} props.slices - An array of slice objects, each containing a `level` property
  * indicating the slice's level (e.g., "green", "yellow", "red").
  * @param {boolean} [props.disableEffects] - If true, disables hover effects on the slices.
  * @param {string} [props.className] - Additional CSS class names to apply to the pizza container.
@@ -49,10 +49,11 @@ const Pizza = ({
     "rotate-[337.5deg]",
   ] as const
 
-  const COLORS: { [key in SeverityLevel]: string } = {
+  const COLORS: { [key in SeverityLevel | "none"]: string } = {
     green: "text-level-best",
     yellow: "text-level-middle",
     red: "text-level-worst",
+    none: "text-body-secondary",
   }
 
   const GROUP_HOVER = [
@@ -80,7 +81,7 @@ const Pizza = ({
           className={cn(
             sharedClasses,
             ROTATIONS[idx % ROTATIONS.length],
-            COLORS[level],
+            COLORS[level || "none"],
             !disableEffects && GROUP_HOVER[idx]
           )}
           key={idx}
diff --git a/components/SoftwareAccordion.tsx b/components/SoftwareAccordion.tsx
index 4803bc02..e81a0938 100644
--- a/components/SoftwareAccordion.tsx
+++ b/components/SoftwareAccordion.tsx
@@ -18,7 +18,7 @@ import Pizza from "./Pizza"
 import SoftwareDetails from "./SoftwareDetails"
 
 import { formatShortDate } from "@/lib/date"
-import { getSoftwareDetailItems, getZkvmsMetrics } from "@/lib/metrics"
+import { getSoftwareDetailItems, getZkvmsMetricsByZkvmId } from "@/lib/metrics"
 import { getSlices, getZkvmsWithUsage } from "@/lib/zkvms"
 
 const SoftwareAccordionItem = ({
@@ -32,7 +32,7 @@ const SoftwareAccordionItem = ({
     totalClusters: number
     activeClusters: number
   }
-  metrics: ZkvmMetrics
+  metrics: Partial<ZkvmMetrics>
 }) => {
   const detailItems = getSoftwareDetailItems(metrics)
 
@@ -101,10 +101,10 @@ const SoftwareAccordionItem = ({
 }
 
 const SoftwareAccordion = async () => {
-  const [zkvms, metricsByZkvmId] = await Promise.all([
-    getZkvmsWithUsage(),
-    getZkvmsMetrics(),
-  ])
+  const zkvms = await getZkvmsWithUsage()
+  const metricsByZkvmId = await getZkvmsMetricsByZkvmId({
+    zkvmIds: zkvms.map((zkvm) => zkvm.id),
+  })
 
   return (
     <Accordion
@@ -139,7 +139,7 @@ const SoftwareAccordion = async () => {
           key={zkvm.id}
           value={"item-" + zkvm.id}
           zkvm={zkvm}
-          metrics={metricsByZkvmId[zkvm.id]}
+          metrics={metricsByZkvmId.get(zkvm.id) || {}}
         />
       ))}
     </Accordion>
diff --git a/lib/api/metrics.ts b/lib/api/metrics.ts
index 545eb7a6..49d4b897 100644
--- a/lib/api/metrics.ts
+++ b/lib/api/metrics.ts
@@ -1,16 +1,20 @@
-import { eq } from "drizzle-orm"
+import { eq, inArray } from "drizzle-orm"
 
 import { db } from "@/db"
-import { zkvmPerformanceMetrics, zkvmSecurityMetrics } from "@/db/schema"
-
-export const getZkvmSecurityMetrics = async () => {
-  const metrics = await db.query.zkvmSecurityMetrics.findMany()
-
-  return metrics
-}
-
-export const getZkvmPerformanceMetrics = async () => {
-  const metrics = await db.query.zkvmPerformanceMetrics.findMany()
+import { zkvmPerformanceMetrics, zkvms, zkvmSecurityMetrics } from "@/db/schema"
+
+export const getZkvmsWithMetrics = async ({
+  zkvmIds,
+}: {
+  zkvmIds: number[]
+}) => {
+  const metrics = await db.query.zkvms.findMany({
+    where: inArray(zkvms.id, zkvmIds),
+    with: {
+      security_metrics: true,
+      performance_metrics: true,
+    },
+  })
 
   return metrics
 }
diff --git a/lib/metrics.tsx b/lib/metrics.tsx
index 47038eb7..4b087c2e 100644
--- a/lib/metrics.tsx
+++ b/lib/metrics.tsx
@@ -1,11 +1,9 @@
-import PizzaSliceEighth from "@/components/svgs/pizza-slice-n-8.svg"
 import ZkvmPopoverDetails from "@/components/ZkvmPopoverDetails"
 
 import {
-  getZkvmPerformanceMetrics,
   getZkvmPerformanceMetricsByZkvmId,
-  getZkvmSecurityMetrics,
   getZkvmSecurityMetricsByZkvmId,
+  getZkvmsWithMetrics,
 } from "./api/metrics"
 import { ZKVM_THRESHOLDS } from "./constants"
 import {
@@ -58,9 +56,11 @@ const trustedSetupLabels: Record<SeverityLevel, string> = {
 }
 
 export const getZkvmMetricLabel = (
-  severity: SeverityLevel,
+  severity: SeverityLevel | undefined,
   metricType: string
 ): string => {
+  if (severity === undefined) return "no data"
+
   switch (metricType) {
     case "protocol_soundness":
     case "implementation_soundness":
@@ -104,25 +104,24 @@ export const getZkvmMetricSeverity = (
 }
 
 export const getZkvmMetricSeverityLevels = (
-  metrics: ZkvmMetrics
-): Record<string, SeverityLevel> => {
+  metrics: Partial<ZkvmMetrics>
+): Record<string, SeverityLevel | undefined> => {
   // Get the severity levels for each numeric metric
-  const proofSizeSeverity = getZkvmMetricSeverity(
-    metrics.size_bytes,
-    "size_bytes"
-  )
-  const verificationTimeSeverity = getZkvmMetricSeverity(
-    metrics.verification_ms,
-    "verification_ms"
-  )
-  const maxBountyAmountSeverity = getZkvmMetricSeverity(
-    metrics.max_bounty_amount,
-    "max_bounty_amount"
-  )
-  const securityTargetSeverity = getZkvmMetricSeverity(
-    metrics.security_target_bits,
-    "security_target_bits"
-  )
+  const proofSizeSeverity = metrics.size_bytes
+    ? getZkvmMetricSeverity(metrics.size_bytes, "size_bytes")
+    : undefined
+  const verificationTimeSeverity = metrics.verification_ms
+    ? getZkvmMetricSeverity(metrics.verification_ms, "verification_ms")
+    : undefined
+  const maxBountyAmountSeverity = metrics.max_bounty_amount
+    ? getZkvmMetricSeverity(metrics.max_bounty_amount, "max_bounty_amount")
+    : undefined
+  const securityTargetSeverity = metrics.security_target_bits
+    ? getZkvmMetricSeverity(
+        metrics.security_target_bits,
+        "security_target_bits"
+      )
+    : undefined
 
   return {
     proofSize: proofSizeSeverity,
@@ -137,38 +136,29 @@ export const getZkvmMetricSeverityLevels = (
   }
 }
 
-export const getZkvmsMetrics = async () => {
-  const securityMetrics = await getZkvmSecurityMetrics()
-  const performanceMetrics = await getZkvmPerformanceMetrics()
-
-  const metricsByZkvmId = new Map()
-
-  for (const metric of securityMetrics) {
-    metricsByZkvmId.set(metric.zkvm_id, {
-      ...metric,
-    })
-  }
+export const getZkvmsMetricsByZkvmId = async ({
+  zkvmIds,
+}: {
+  zkvmIds: number[]
+}) => {
+  const zkvmsWithMetrics = await getZkvmsWithMetrics({ zkvmIds })
 
-  for (const metric of performanceMetrics) {
-    const existing = metricsByZkvmId.get(metric.zkvm_id) || {}
+  const metricsByZkvmId = new Map<number, Partial<ZkvmMetrics>>()
 
-    metricsByZkvmId.set(metric.zkvm_id, {
-      ...existing,
-      ...metric,
+  for (const zkvm of zkvmsWithMetrics) {
+    metricsByZkvmId.set(zkvm.id, {
+      ...zkvm.security_metrics,
+      ...zkvm.performance_metrics,
     })
   }
 
-  return Object.fromEntries(metricsByZkvmId)
+  return metricsByZkvmId
 }
 
 export const getZkvmMetrics = async (zkvmId: number) => {
   const securityMetrics = await getZkvmSecurityMetricsByZkvmId(zkvmId)
   const performanceMetrics = await getZkvmPerformanceMetricsByZkvmId(zkvmId)
 
-  if (!securityMetrics || !performanceMetrics) {
-    throw new Error("No metrics found for zkvm")
-  }
-
   return {
     ...securityMetrics,
     ...performanceMetrics,
@@ -176,7 +166,7 @@ export const getZkvmMetrics = async (zkvmId: number) => {
 }
 
 export const getSoftwareDetailItems = (
-  metrics: ZkvmMetrics
+  metrics: Partial<ZkvmMetrics>
 ): SoftwareDetailItem[] => {
   const severityLevels = getZkvmMetricSeverityLevels(metrics)
 
@@ -201,6 +191,7 @@ export const getSoftwareDetailItems = (
         worstThreshold: ZKVM_THRESHOLDS.verification_ms.red,
         unit: "ms",
         value: Number(metrics.verification_ms),
+        disabled: !metrics.verification_ms,
       },
       className: "px-8",
     },
@@ -227,6 +218,7 @@ export const getSoftwareDetailItems = (
         worstThreshold: ZKVM_THRESHOLDS.size_bytes.red / 1024,
         unit: "kB",
         value: Number(metrics.size_bytes) / 1024,
+        disabled: !metrics.size_bytes,
       },
       className: "px-8",
     },
diff --git a/lib/types.ts b/lib/types.ts
index d74c5df7..727aec1e 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -231,7 +231,7 @@ export type Stats = {
 }
 
 export type SliceDetails = {
-  level: SeverityLevel
+  level: SeverityLevel | undefined
 }
 
 export type Slices = [
@@ -252,7 +252,7 @@ export type SoftwareDetailItem = {
   label: React.ReactNode
   className?: string
   popoverDetails: React.ReactNode
-  severity: SeverityLevel
+  severity: SeverityLevel | undefined
   position: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
 } & ({ chartInfo: LevelMeterProps } | { value: string })
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0ed681b75e810e8c4b63780f5b3a42174832b784 and 04cce55e7b3bc681eff9700004dab2e3ef8283f3.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `components/LevelMeter.tsx` (3 hunks)
* `components/Pizza.tsx` (3 hunks)
* `components/SoftwareAccordion.tsx` (4 hunks)
* `lib/api/metrics.ts` (1 hunks)
* `lib/metrics.tsx` (6 hunks)
* `lib/types.ts` (2 hunks)
* `styles/semantic-tokens.css` (1 hunks)
* `tailwind.config.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (4)</summary>

<details>
<summary>components/Pizza.tsx (1)</summary><blockquote>

<details>
<summary>lib/types.ts (1)</summary>

* `SeverityLevel` (136-136)

</details>

</blockquote></details>
<details>
<summary>components/LevelMeter.tsx (1)</summary><blockquote>

<details>
<summary>lib/utils.ts (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>
<details>
<summary>lib/api/metrics.ts (2)</summary><blockquote>

<details>
<summary>db/index.ts (1)</summary>

* `db` (17-21)

</details>
<details>
<summary>db/schema.ts (1)</summary>

* `zkvms` (414-436)

</details>

</blockquote></details>
<details>
<summary>components/SoftwareAccordion.tsx (3)</summary><blockquote>

<details>
<summary>lib/types.ts (1)</summary>

* `ZkvmMetrics` (138-138)

</details>
<details>
<summary>lib/zkvms.ts (1)</summary>

* `getZkvmsWithUsage` (18-35)

</details>
<details>
<summary>lib/metrics.tsx (1)</summary>

* `getZkvmsMetricsByZkvmId` (139-156)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Redirect rules - ethproofs
* GitHub Check: Header rules - ethproofs
* GitHub Check: Pages changed - ethproofs

</details>

<details>
<summary>🔇 Additional comments (24)</summary><blockquote>

<details>
<summary>tailwind.config.ts (1)</summary>

`76-76`: **Good addition of fallback styling for undefined severity levels.**

Adding a dedicated color for the `none` state is a clean approach to handling missing metric data. This enhances the UI by providing visual feedback when no severity level is available, rather than failing silently.

</details>
<details>
<summary>styles/semantic-tokens.css (1)</summary>

`43-43`: **Good choice of neutral gray color for undefined states.**

The medium gray color (`0, 0%, 55%`) is a sensible choice for representing missing data. It's visually distinct from the severity levels while remaining unobtrusive in the UI.

</details>
<details>
<summary>lib/types.ts (2)</summary>

`234-235`: **Good type definition update to handle optional severity levels.**

Making the `level` property optional in `SliceDetails` aligns with the PR objective of handling missing zkvm metrics state. This change enables proper type checking throughout the application when dealing with undefined metric data.

---

`255-256`: **Consistent approach to handling optional severity levels.**

The update to `SoftwareDetailItem` to allow undefined severity values maintains consistency with the `SliceDetails` type changes. This ensures a uniform approach to handling missing metric data across different parts of the application.

</details>
<details>
<summary>components/Pizza.tsx (3)</summary>

`17-18`: **Corrected JSDoc parameter type to match implementation.**

The JSDoc comment now correctly references the `Slices` type which is imported and used as the type for the `slices` prop. This improves documentation accuracy.

---

`52-57`: **Good implementation of fallback styling for undefined levels.**

The `COLORS` mapping has been properly extended to include the `"none"` case, with appropriate typing to support both defined severity levels and the fallback case.

---

`84-85`: **Robust fallback handling for undefined severity levels.**

Using the `level || "none"` pattern provides a clean fallback mechanism when the severity level is undefined. This ensures that the UI remains visually consistent even when metric data is missing.

</details>
<details>
<summary>components/LevelMeter.tsx (4)</summary>

`8-8`: **Good addition of the disabled prop.**

This optional boolean prop will allow the component to properly handle cases when metric data is missing or undefined.

---

`16-16`: **LGTM: Proper prop destructuring.**

The `disabled` prop is correctly destructured from the component props.

---

`49-54`: **Well-implemented conditional styling.**

Good use of the `cn` utility function to conditionally apply the "grayscale" class when the component is disabled, maintaining the existing classes and adding the custom className.

---

`81-101`: **Excellent conditional rendering approach.**

The conditional rendering of the current-value indicator is well implemented, ensuring that when the component is disabled, the user won't see the value marker that might be misleading in the absence of valid data.

</details>
<details>
<summary>lib/api/metrics.ts (3)</summary>

`1-1`: **Good import of necessary utilities.**

Adding the `inArray` import is appropriate for filtering by multiple IDs in the new function.

---

`4-4`: **Complete schema import.**

Proper import of all necessary schema entities, including the main `zkvms` table which is now needed for the direct query.

---

`6-20`: **Well-designed batch metrics fetching function.**

This new function efficiently retrieves multiple zkvms with their associated metrics in a single database query. The eager loading of related data through the `with` clause is an optimal approach.

</details>
<details>
<summary>components/SoftwareAccordion.tsx (3)</summary>

`21-21`: **Updated import for unified metrics API.**

Correctly updated to use the new metrics fetching function.

---

`35-35`: **Good type relaxation to handle partial data.**

Updating the type to `Partial<ZkvmMetrics>` appropriately reflects that metrics data might be incomplete, aligning with the changes in metrics handling throughout the application.

---

`105-107`: **Efficient batch metrics fetching.**

Good implementation of the new approach to fetch metrics for all ZKVMs at once by passing their IDs to the unified API.

</details>
<details>
<summary>lib/metrics.tsx (7)</summary>

`6-6`: **Updated import for the new metrics API.**

Correctly updated to include the new batch metrics fetching function.

---

`59-63`: **Improved handling of undefined severity.**

Good enhancement to handle undefined severity levels, returning "no data" when no metric data is available.

---

`107-108`: **Updated function signature for partial metrics support.**

Properly updated to accept `Partial<ZkvmMetrics>` and to return undefined severity levels when data is missing.

---

`110-125`: **Robust null checking for metric values.**

Well-implemented conditional checks that verify each metric value exists before calculating its severity level.

---

`139-156`: **Well-implemented unified metrics retrieval function.**

This function efficiently fetches and combines metrics for multiple ZKVMs, returning a Map structure that makes it easy to look up metrics by ZKVM ID.

---

`169-170`: **Updated function signature for partial metrics support.**

Correctly updated to accept `Partial<ZkvmMetrics>`, allowing the function to work with incomplete metric data.

---

`194-194`: **Good addition of disabled flags for missing metrics.**

Adding these flags ensures chart components can visually indicate when data is missing or incomplete, providing a better user experience.



Also applies to: 221-221

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-16T13:17:33Z
user: login: wackerow
body: Redundant color token; will patch to just use `body-secondary`
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T19:47:42Z
user: login: wackerow
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T19:55:11Z

Review comments:
diff_hunk: @@ -139,7 +139,7 @@ const SoftwareAccordion = async () => {
           key={zkvm.id}
           value={"item-" + zkvm.id}
           zkvm={zkvm}
-          metrics={metricsByZkvmId[zkvm.id]}
+          metrics={metricsByZkvmId.get(zkvm.id)!}
path: components/SoftwareAccordion.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove non-null assertion to handle missing metrics safely.**

The current implementation assumes metrics will always exist for every ZKVM ID, which may not be true if the API returns partial data or if a ZKVM has no metrics.

Consider this safer alternative that handles potentially missing metrics:

```diff
-          metrics={metricsByZkvmId.get(zkvm.id)!}
+          metrics={metricsByZkvmId.get(zkvm.id) || {}}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion
          metrics={metricsByZkvmId.get(zkvm.id) || {}}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In components/SoftwareAccordion.tsx at line 142, remove the non-null assertion
operator (!) from metricsByZkvmId.get(zkvm.id) to avoid assuming metrics always
exist. Instead, add a conditional check or fallback to handle cases where
metrics might be undefined or missing, ensuring the component safely handles
partial or absent data without runtime errors.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 62beadd
created_at: 2025-05-16T13:17:32Z
author_association: NONE
diff_hunk: @@ -40,6 +40,7 @@
     --level-best: 136, 100%, 38%; /* #1DC200 */
     --level-middle: 54, 100%, 50%; /* #FFF200 */
     --level-worst: 0, 100%, 40%; /* #CC0000 */
+    --level-none: 0, 0%, 55%; /* #8c8c8c */
path: styles/semantic-tokens.css
user: login: wackerow
body: This is the same as `--body-secondary` (`gray-400`) which is already declared above.. It's been the go-to "gray" text color throughout

```suggestion
```
created_at: 2025-05-16T19:44:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,6 +73,7 @@ const config = {
           best: "hsla(var(--level-best))",
           middle: "hsla(var(--level-middle))",
           worst: "hsla(var(--level-worst))",
+          none: "hsla(var(--level-none))",
path: tailwind.config.ts
user: login: wackerow
body: ```suggestion
```
created_at: 2025-05-16T19:44:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,10 +49,11 @@ const Pizza = ({
     "rotate-[337.5deg]",
   ] as const
 
-  const COLORS: { [key in SeverityLevel]: string } = {
+  const COLORS: { [key in SeverityLevel | "none"]: string } = {
     green: "text-level-best",
     yellow: "text-level-middle",
     red: "text-level-worst",
+    none: "text-level-none",
path: components/Pizza.tsx
user: login: wackerow
body: ```suggestion
    none: "text-body-secondary",
```
created_at: 2025-05-16T19:45:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-16T13:14:51Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: failure by coderabbit.ai -->

> [!CAUTION]
> ## Review failed
> 
> The pull request is closed.

<!-- end of auto-generated comment: failure by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes introduce improved handling of missing or partial metric data across several components and utility functions. Metric-fetching logic is unified to support batch retrieval by zkvm IDs, component props and type definitions are adjusted to allow undefined severity levels, and UI elements now use a `disabled` state when data is unavailable. Some obsolete functions are removed and type signatures are updated for greater flexibility.

## Changes

| File(s)                                         | Change Summary |
|------------------------------------------------|----------------|
| `components/LevelMeter.tsx`                     | Added optional `disabled` prop to `LevelMeter`; applies a grayscale class and hides the value indicator when disabled. |
| `components/Pizza.tsx`                          | Updated JSDoc and type for `slices` prop; expanded `COLORS` constant to include a "none" fallback; color assignment uses fallback when level is undefined. |
| `components/SoftwareAccordion.tsx`              | Switched metrics fetching to use `getZkvmsMetricsByZkvmId`; metrics prop type relaxed to allow partial data; metrics now accessed via a Map. |
| `lib/api/metrics.ts`                            | Removed separate security/performance metrics fetchers; added `getZkvmsWithMetrics` for batch retrieval; updated imports. |
| `lib/metrics.tsx`                               | Unified metrics fetching via `getZkvmsWithMetrics`; refactored functions to handle partial/missing data; updated function signatures and UI logic to support disabled state when data is missing. |
| `lib/types.ts`                                  | Changed `SliceDetails.level` and `SoftwareDetailItem.severity` types to allow `undefined`. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant UI as UI Component (e.g., SoftwareAccordion)
    participant MetricsLib as metrics.tsx
    participant API as api/metrics.ts

    UI->>MetricsLib: getZkvmsMetricsByZkvmId({ zkvmIds })
    MetricsLib->>API: getZkvmsWithMetrics({ zkvmIds })
    API-->>MetricsLib: Map<zkvmId, Partial<ZkvmMetrics>>
    MetricsLib-->>UI: Map<zkvmId, Partial<ZkvmMetrics>>
    UI->>UI: Render components (some with disabled state if metrics missing)
```

## Poem

> In the garden where metrics grow,  
> Rabbits fetch them in a single row.  
> If data’s missing, don’t despair—  
> A grayscale meter shows we care.  
> Undefined severity? That’s just fine,  
> With fallback colors, all will shine!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bc58222929f2c8a6c285b49467671a83ad0adbec and 62beadd3c9fbc304e2c3e077c2403920105e987d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `components/SoftwareAccordion.tsx` (4 hunks)

</details>

</details>
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=ethproofs/ethproofs&utm_content=381):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: netlify[bot]
created_at: 2025-05-16T13:14:51Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *ethproofs* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 62beadd3c9fbc304e2c3e077c2403920105e987d |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/ethproofs/deploys/6827981a40f85c00089dcf5f |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-381--ethproofs.netlify.app](https://deploy-preview-381--ethproofs.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTM4MS0tZXRocHJvb2ZzLm5ldGxpZnkuYXBwIn0.qwPBddR2mwtGBLwM8prKle54bsUESzTAJKlShFd4_tU)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [ethproofs Preview](https://deploy-preview-381--ethproofs.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify project configuration](https://app.netlify.com/projects/ethproofs/configuration/notifications#deploy-notifications)._
author_association: NONE