diff --git a/src/app/insights/sleep/page.tsx b/src/app/insights/sleep/page.tsx
index 1cac47cd7..8b2845275 100644
--- a/src/app/insights/sleep/page.tsx
+++ b/src/app/insights/sleep/page.tsx
@@ -13,6 +13,7 @@ import { MetricEmptyState } from "@/components/insights/metric-empty-state";
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
import { SleepOverview } from "@/components/insights/sleep-overview";
import { SleepRhythmSection } from "@/components/insights/sleep/sleep-rhythm-section";
+import { SleepQualitySection } from "@/components/insights/sleep/sleep-quality-section";
import { SubPageShell } from "@/components/insights/sub-page-shell";
/**
@@ -89,6 +90,16 @@ export default function InsightsSchlafPage() {
*/}
+ {/*
+ v1.17.1 — "Sleep quality" block. Surfaces the WHOOP / Oura nightly
+ sleep-quality scores (efficiency / performance / consistency / need /
+ disturbance count + the Oura sleep score) that were ingested but never
+ rendered. Each tile is data-gated, so the block is invisible for
+ non-wearable users and collapses metric-by-metric. Server-authoritative
+ — the tiles render stored values.
+ */}
+
+
{/*
diff --git a/src/components/insights/sleep/sleep-quality-section.tsx b/src/components/insights/sleep/sleep-quality-section.tsx
new file mode 100644
index 000000000..0b6a817ed
--- /dev/null
+++ b/src/components/insights/sleep/sleep-quality-section.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { Activity, Gauge, Moon, Repeat, Target, Waves } from "lucide-react";
+
+import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
+import { useTranslations } from "@/lib/i18n/context";
+import { SectionHeading } from "@/components/insights/section-heading";
+import { DeviceScoreTile } from "@/components/insights/device-score-tile";
+
+/**
+ * v1.17.1 — "Sleep quality" block on `/insights/sleep`.
+ *
+ * Surfaces the WHOOP / Oura nightly sleep-quality scores that were ingested
+ * end-to-end but rendered nowhere: efficiency, performance, consistency, need,
+ * disturbance count, and the Oura headline sleep score. Each tile is gated on
+ * the metric having data (`` returns null at count 0), so a
+ * non-wearable user never sees an empty card and the whole section collapses
+ * when none of the six has readings.
+ *
+ * Reads the shared `["analytics", "summaries"]` slice — the same cache the
+ * page's duration gate already populated, so this adds no network round-trip.
+ * Server-authoritative: the tiles render the stored values, never a recompute.
+ */
+export function SleepQualitySection({ enabled }: { enabled: boolean }) {
+ const { t } = useTranslations();
+ const { data } = useInsightsAnalytics("SLEEP_DURATION");
+
+ if (!enabled) return null;
+
+ const summaries = data?.summaries;
+ if (!summaries) return null;
+
+ const tiles: Array<{
+ type: string;
+ title: string;
+ icon: typeof Moon;
+ color: string;
+ unit?: string;
+ }> = [
+ {
+ type: "SLEEP_SCORE",
+ title: t("measurements.typeSleepScore"),
+ icon: Moon,
+ color: "#bd93f9",
+ unit: t("insights.deviceScore.unitScore"),
+ },
+ {
+ type: "SLEEP_PERFORMANCE",
+ title: t("measurements.typeSleepPerformance"),
+ icon: Target,
+ color: "#8be9fd",
+ unit: "%",
+ },
+ {
+ type: "SLEEP_EFFICIENCY",
+ title: t("measurements.typeSleepEfficiency"),
+ icon: Gauge,
+ color: "#50fa7b",
+ unit: "%",
+ },
+ {
+ type: "SLEEP_CONSISTENCY",
+ title: t("measurements.typeSleepConsistency"),
+ icon: Repeat,
+ color: "#f1fa8c",
+ unit: "%",
+ },
+ {
+ type: "SLEEP_NEED",
+ title: t("measurements.typeSleepNeed"),
+ icon: Waves,
+ color: "#ffb86c",
+ unit: t("insights.deviceScore.unitMinutes"),
+ },
+ {
+ type: "SLEEP_DISTURBANCE_COUNT",
+ title: t("measurements.typeSleepDisturbanceCount"),
+ icon: Activity,
+ color: "#ff79c6",
+ },
+ ];
+
+ const present = tiles.filter(
+ (tile) => (summaries[tile.type]?.count ?? 0) > 0,
+ );
+ if (present.length === 0) return null;
+
+ return (
+
+
+ {t("insights.sleepQuality.subtitle")}
+
+ }
+ />
+
+ {present.map((tile) => (
+
+ ))}
+
+
+ );
+}