diff --git a/src/app/[locale]/dashboard/page.tsx b/src/app/[locale]/dashboard/page.tsx
index 06637e9..57e0f03 100644
--- a/src/app/[locale]/dashboard/page.tsx
+++ b/src/app/[locale]/dashboard/page.tsx
@@ -11,6 +11,7 @@ import {
import { personalAccountAtCapacity } from "@/lib/personal-org";
import { Card, CardHeader } from "@/components/ui/card";
import { StatusBadge } from "@/components/ui/status-badge";
+import { WarningBadge } from "@/components/ui/warning-badge";
import { OnboardingFlow } from "@/components/onboarding/onboarding-flow";
import { ProvisioningStatus } from "@/components/onboarding/provisioning-status";
import { formatDateTime } from "@/lib/format";
@@ -348,7 +349,10 @@ export default async function DashboardPage() {
{tenant.metadata.name}
-
diff --git a/src/components/ui/warning-badge.tsx b/src/components/ui/warning-badge.tsx
new file mode 100644
index 0000000..fb288ec
--- /dev/null
+++ b/src/components/ui/warning-badge.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+
+/**
+ * Tenant warning shape received from the operator's status.warnings.
+ * Mirror of the operator's `TenantWarning` type. See
+ * pieced-operator/api/v1alpha1/piecedtenant_types.go.
+ */
+export interface TenantWarning {
+ source: string;
+ reason?: string;
+ message?: string;
+ since?: string;
+}
+
+interface Props {
+ warnings: TenantWarning[];
+}
+
+/**
+ * Renders a small amber warning badge if there are any non-fatal
+ * warnings on the tenant. The badge sits visually next to the phase
+ * StatusBadge — they're separate concepts (phase = lifecycle, warnings
+ * = observed sub-issues) and may both be present at once (e.g. tenant
+ * is `Ready` but has a SkillPacksReady=False warning).
+ *
+ * Hover/focus reveals the warning detail. We don't truncate the message
+ * inside the tooltip; OCI/CRD condition messages tend to be short and
+ * include the actionable detail (which skill, which secret, which
+ * resolver). If a future warning source has a 5-line stacktrace as a
+ * message we'll need a different treatment; cross that bridge then.
+ *
+ * Returns null when there are no warnings — keep render-call sites
+ * simple, they don't have to gate on length themselves.
+ */
+export function WarningBadge({ warnings }: Props) {
+ const t = useTranslations("warnings");
+ if (!warnings || warnings.length === 0) return null;
+
+ const tooltipLabel = (() => {
+ try {
+ return warnings.length === 1
+ ? t("oneTooltip")
+ : t("manyTooltip", { count: warnings.length });
+ } catch {
+ return warnings.length === 1
+ ? "1 warning"
+ : `${warnings.length} warnings`;
+ }
+ })();
+
+ return (
+
+
+
+ {/*
+ Tooltip. Hidden by default; shown on hover OR focus of the
+ sibling button. Positioned below-right so it doesn't collide with
+ the StatusBadge that typically sits left of this. Constrained
+ width so long messages wrap.
+ z-50 keeps it above table rows / cards.
+ */}
+
+ {warnings.map((w, i) => (
+
+