Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 116 additions & 2 deletions src/app/golf/(dashboard)/dashboard/intelligence/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,119 @@
import { GenericPageSkeleton } from '@/components/ui/skeleton';
import { fairwayScope } from '@/lib/redesign/flag';
import { Skeleton } from '@/components/fairway/feedback/Skeleton';

/**
* Fairway-scoped route skeleton for the Team Brief (/dashboard/intelligence).
* Fixes #922 (Phase 1) — replaces the pre-Fairway `GenericPageSkeleton` (a
* generic warm-200 card grid that reshaped the page on mount → CLS) with a
* shape-matched reservation of the ACTUAL `FairwayBrief.tsx` layout: the
* "Work on this first" hero (directive + Readout chip + button on the left,
* a Ribbon/bar-strip slot on the right), the strengths/weaknesses two-column
* split, the demoted pulse strip, the 5 category-detail rows, and the
* collapsed "Deep analysis" disclosure bar — same pattern as
* `dashboard/insights/loading.tsx` and `FairwayBrief.tsx`'s own
* `DeepAnalysisSkeleton` (rendered later, under the disclosure).
*/
export default function IntelligenceLoading() {
return <GenericPageSkeleton />;
return (
<div className={fairwayScope('min-h-full bg-canvas bg-canvas-gradient font-fw-sans')}>
<div
role="status"
aria-busy="true"
aria-live="polite"
className="mx-auto flex max-w-5xl flex-col gap-8 px-4 py-6 md:px-6"
>
<span className="sr-only">Loading team brief…</span>

{/* masthead */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-7 w-48" />
</div>
<Skeleton className="h-9 w-28 rounded-fw-md" />
</div>

{/* 1 · work-on-this-first hero */}
<div className="rounded-card border border-border-subtle bg-surface p-6 md:p-8">
<Skeleton className="mb-5 h-3 w-32" />
<div className="grid gap-6 md:grid-cols-[1.1fr_0.9fr]">
<div className="flex flex-col gap-4">
<Skeleton className="h-8 w-5/6" />
<Skeleton className="h-8 w-1/2" />
<div className="flex items-end gap-3 pt-1">
<Skeleton className="h-10 w-20 rounded-fw-md" />
<Skeleton className="h-6 w-36 rounded-full" />
</div>
<Skeleton className="h-4 w-4/5" />
<Skeleton className="mt-1 h-9 w-40 rounded-fw-md" />
</div>
<Skeleton className="h-44 w-full rounded-fw-md" />
</div>
<div className="mt-5 flex flex-col gap-2 border-t border-border-subtle pt-4">
<Skeleton className="h-3 w-40" />
<div className="flex flex-wrap gap-2">
<Skeleton className="h-7 w-24 rounded-full" />
<Skeleton className="h-7 w-28 rounded-full" />
<Skeleton className="h-7 w-20 rounded-full" />
</div>
</div>
</div>

{/* 2 · strengths & weaknesses */}
<div className="flex flex-col gap-3">
<div className="flex items-baseline justify-between">
<Skeleton className="h-5 w-44" />
<Skeleton className="h-3 w-36" />
</div>
<div className="grid gap-4 md:grid-cols-2">
{[0, 1].map((col) => (
<div key={col} className="rounded-card border border-border-subtle bg-surface p-5">
<Skeleton className="mb-3 h-3 w-24" />
<div className="flex flex-col gap-2">
{[0, 1].map((row) => (
<Skeleton key={row} className="h-11 w-full rounded-fw-md" />
))}
</div>
</div>
))}
</div>
</div>

{/* 3 · team pulse strip */}
<div className="rounded-card border border-border-subtle bg-surface p-5">
<div className="flex flex-wrap items-center gap-x-8 gap-y-3">
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-24" />
<Skeleton className="h-3 w-56" />
</div>
</div>

{/* 4 · category detail rows */}
<div className="flex flex-col gap-3">
<div className="flex items-baseline justify-between">
<Skeleton className="h-5 w-32" />
<Skeleton className="h-3 w-28" />
</div>
<div className="flex flex-col gap-3">
{[0, 1, 2, 3, 4].map((i) => (
<div key={i} className="rounded-card border border-border-subtle bg-surface p-5">
<div className="flex items-start gap-3">
<Skeleton circle className="h-9 w-9 shrink-0" />
<div className="flex-1 space-y-2">
<Skeleton className="h-3 w-28" />
<Skeleton className="h-5 w-2/3" />
</div>
</div>
<Skeleton className="mt-3 h-11 w-full rounded-fw-md" />
<Skeleton className="mt-3 h-8 w-28 rounded-fw-md" />
</div>
))}
</div>
</div>

{/* 5 · collapsed deep-analysis disclosure bar */}
<Skeleton className="h-16 w-full rounded-card" />
</div>
</div>
);
}
52 changes: 52 additions & 0 deletions src/app/golf/actions/team-category-insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { logServerError } from '@/lib/server-error-logger';
import { fetchAllRowsResult } from '@/lib/supabase/fetch-all-rows';
import { resolveCoachTeamIdWithCookie } from '@/lib/golf/resolve-team-server';
import { withAdminObserved } from '@/lib/admin/observed-action';
import { getInsightsForCoachWithMeta } from '@/app/golf/actions/insight-delivery';
import {
assembleBriefEngineInsights,
briefEngineCategories,
} from '@/lib/coachhelm/v3/brief/assemble';
import {
samplePerPlayerRounds,
computeTeamHealth,
Expand All @@ -22,6 +27,16 @@ export interface CategoryInsight {
metric?: string;
value?: number;
benchmark?: number;
/** Fixes #922 (Phase 1) — set when this sentence was assembled from a real,
* visibility-filtered `golf_coach_insights` engine row (via
* `getInsightsForCoachWithMeta` + `assembleBriefEngineInsights`), not the
* hand-written `generateCategoryInsights` template below. Presentation-only:
* the UI badges an engine-backed row with its real strokes-saved figure. */
engineBacked?: boolean;
/** Realistic strokes-saved-per-round from `evidence.counterfactual`, present
* only on an `engineBacked` row with a live (non-suppressed) counterfactual.
* `null` on an engine-backed but diagnostic-only row; absent on a template row. */
strokesSavedPerRound?: number | null;
}

export interface PlayerCategoryStat {
Expand Down Expand Up @@ -908,6 +923,43 @@ async function getTeamCategoryInsightsImpl(
});
}

// 5b. Fixes #922 (Phase 1) — read-time-only engine-sentence assembler.
// Swap the hand-written `insights[0]` template sentence for a genuine
// engine-backed one (evidence.counterfactual/standing on real
// golf_coach_insights rows) when the team has a visible engine row for
// that category. Reuses the EXACT same insight-delivery read path (rank +
// dedupe + `applyInsightVisibility`) the Signals surfaces render from, via
// the team-wide sweep (`getInsightsForCoachWithMeta`, no player_id — RLS
// scopes it to teams the coach staffs). ADDITIVE: does not touch the
// trend/rating math above (PR #929 territory) — only appends to
// `categories[].insights`. Best-effort: any failure here degrades to the
// template-only insights already computed, never breaks the read.
try {
const engineResult = await getInsightsForCoachWithMeta(
session.coach.id,
{ categories: briefEngineCategories() },
supabase,
);
if (engineResult.ok) {
const engineByCategory = assembleBriefEngineInsights(
engineResult.data,
CATEGORIES.map((c) => ({ id: c.id, label: c.label })),
);
for (const cat of categories) {
const engineInsight = engineByCategory.get(cat.id);
if (engineInsight) {
cat.insights = [engineInsight, ...cat.insights].slice(0, 3);
}
}
}
} catch (err) {
await logServerError(
`getTeamCategoryInsights engine enrichment failed (continuing with template insights): ${err instanceof Error ? err.message : String(err)}`,
{ action: 'getTeamCategoryInsights', featureArea: 'insights' },
'warning',
);
}

// 6. Team health score — only categories that actually have player data
// count (empty = insufficient data, not a perfect score). See P2-17.
const teamHealth = computeTeamHealth(categories);
Expand Down
9 changes: 9 additions & 0 deletions src/components/fairway/pages/coachhelm/FairwayBrief.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,15 @@ function CategoryHealthRow({ cat }: { cat: TeamCategory }) {
)}
/>
<span className="font-fw-sans text-body-sm text-text-secondary">{evidence.message}</span>
{/* Fixes #922 (Phase 1) — badges a genuine engine-backed sentence
(assembleBriefEngineInsights, from evidence.counterfactual on a
real golf_coach_insights row) with its real strokes-saved figure.
Absent on the hand-written template rows (no fabricated number). */}
{evidence.engineBacked && evidence.strokesSavedPerRound != null ? (
<Badge tone="warning" size="sm" className="ml-auto shrink-0">
{evidence.strokesSavedPerRound.toFixed(1)}/rd
</Badge>
) : null}
</div>
) : null}

Expand Down
Loading
Loading