Add color-coded backgrounds to DecisionCard
All checks were successful
Deploy / deploy (push) Successful in 2m26s

Per dashboard.md spec requirements:
- RED background and text for REST decisions
- YELLOW background and text for GENTLE/LIGHT/REDUCED decisions
- GREEN background and text for TRAIN decisions

Added 8 new tests for color-coded backgrounds (19 total).
Updated IMPLEMENTATION_PLAN.md to mark spec gap as complete.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 23:11:10 +00:00
parent 31932a88bf
commit 5cac8f3267
3 changed files with 148 additions and 16 deletions

View File

@@ -4,7 +4,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
## Current State Summary ## Current State Summary
### Overall Status: 841 tests passing across 44 test files ### Overall Status: 849 tests passing across 44 test files
### Library Implementation ### Library Implementation
| File | Status | Gap Analysis | | File | Status | Gap Analysis |
@@ -113,7 +113,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
| `src/app/settings/page.test.tsx` | **EXISTS** - 34 tests (form rendering, validation, submission, accessibility, logout functionality) | | `src/app/settings/page.test.tsx` | **EXISTS** - 34 tests (form rendering, validation, submission, accessibility, logout functionality) |
| `src/app/api/auth/logout/route.test.ts` | **EXISTS** - 5 tests (cookie clearing, success response, error handling) | | `src/app/api/auth/logout/route.test.ts` | **EXISTS** - 5 tests (cookie clearing, success response, error handling) |
| `src/app/settings/garmin/page.test.tsx` | **EXISTS** - 27 tests (connection status, token management) | | `src/app/settings/garmin/page.test.tsx` | **EXISTS** - 27 tests (connection status, token management) |
| `src/components/dashboard/decision-card.test.tsx` | **EXISTS** - 11 tests (rendering, status icons, styling) | | `src/components/dashboard/decision-card.test.tsx` | **EXISTS** - 19 tests (rendering, status icons, styling, color-coded backgrounds) |
| `src/components/dashboard/data-panel.test.tsx` | **EXISTS** - 18 tests (biometrics display, null handling, styling) | | `src/components/dashboard/data-panel.test.tsx` | **EXISTS** - 18 tests (biometrics display, null handling, styling) |
| `src/components/dashboard/nutrition-panel.test.tsx` | **EXISTS** - 12 tests (seeds, carbs, keto guidance) | | `src/components/dashboard/nutrition-panel.test.tsx` | **EXISTS** - 12 tests (seeds, carbs, keto guidance) |
| `src/components/dashboard/override-toggles.test.tsx` | **EXISTS** - 18 tests (toggle states, callbacks, styling) | | `src/components/dashboard/override-toggles.test.tsx` | **EXISTS** - 18 tests (toggle states, callbacks, styling) |
@@ -702,18 +702,18 @@ Testing, error handling, and refinements.
### P3.11: Missing Component Tests ✅ COMPLETE ### P3.11: Missing Component Tests ✅ COMPLETE
- [x] Add unit tests for untested components - [x] Add unit tests for untested components
- **Components Tested (5 total):** - **Components Tested (5 total):**
- `src/components/dashboard/decision-card.tsx` - 11 tests for rendering decision status, icon, reason, styling - `src/components/dashboard/decision-card.tsx` - 19 tests for rendering decision status, icon, reason, styling, color-coded backgrounds
- `src/components/dashboard/data-panel.tsx` - 18 tests for biometrics display (BB, HRV, intensity), null handling, styling - `src/components/dashboard/data-panel.tsx` - 18 tests for biometrics display (BB, HRV, intensity), null handling, styling
- `src/components/dashboard/nutrition-panel.tsx` - 12 tests for seeds, carbs, keto guidance display - `src/components/dashboard/nutrition-panel.tsx` - 12 tests for seeds, carbs, keto guidance display
- `src/components/dashboard/override-toggles.tsx` - 18 tests for toggle states, callbacks, styling - `src/components/dashboard/override-toggles.tsx` - 18 tests for toggle states, callbacks, styling
- `src/components/calendar/day-cell.tsx` - 23 tests for phase coloring, today highlighting, click handling - `src/components/calendar/day-cell.tsx` - 23 tests for phase coloring, today highlighting, click handling
- **Test Files Created:** - **Test Files Created:**
- `src/components/dashboard/decision-card.test.tsx` - 11 tests - `src/components/dashboard/decision-card.test.tsx` - 19 tests
- `src/components/dashboard/data-panel.test.tsx` - 18 tests - `src/components/dashboard/data-panel.test.tsx` - 18 tests
- `src/components/dashboard/nutrition-panel.test.tsx` - 12 tests - `src/components/dashboard/nutrition-panel.test.tsx` - 12 tests
- `src/components/dashboard/override-toggles.test.tsx` - 18 tests - `src/components/dashboard/override-toggles.test.tsx` - 18 tests
- `src/components/calendar/day-cell.test.tsx` - 23 tests - `src/components/calendar/day-cell.test.tsx` - 23 tests
- **Total Tests Added:** 82 tests across 5 files - **Total Tests Added:** 90 tests across 5 files
- **Why:** Component isolation ensures UI correctness and prevents regressions - **Why:** Component isolation ensures UI correctness and prevents regressions
--- ---
@@ -902,7 +902,7 @@ P4.* UX Polish ────────> After core functionality complete
- [x] **metrics.ts** - Complete with 18 tests (metrics collection, counters, gauges, histograms, Prometheus format) (P2.16) - [x] **metrics.ts** - Complete with 18 tests (metrics collection, counters, gauges, histograms, Prometheus format) (P2.16)
### Components ### Components
- [x] **DecisionCard** - Displays decision status, icon, and reason - [x] **DecisionCard** - Displays decision status, icon, reason with color-coded backgrounds (RED/YELLOW/GREEN per status)
- [x] **DataPanel** - Shows body battery, HRV, intensity data - [x] **DataPanel** - Shows body battery, HRV, intensity data
- [x] **NutritionPanel** - Shows seeds, carbs, keto guidance - [x] **NutritionPanel** - Shows seeds, carbs, keto guidance
- [x] **OverrideToggles** - Toggle buttons for flare/stress/sleep/pms - [x] **OverrideToggles** - Toggle buttons for flare/stress/sleep/pms
@@ -952,7 +952,7 @@ P4.* UX Polish ────────> After core functionality complete
- [x] **P3.7: Error Handling Improvements** - Replaced console.error with structured pino logger across API routes, added key event logging (Period logged, Override toggled, Decision calculated, Auth failure), 3 new tests in auth-middleware.test.ts - [x] **P3.7: Error Handling Improvements** - Replaced console.error with structured pino logger across API routes, added key event logging (Period logged, Override toggled, Decision calculated, Auth failure), 3 new tests in auth-middleware.test.ts
- [x] **P3.8: Loading States** - Complete with skeleton components (DecisionCardSkeleton, DataPanelSkeleton, NutritionPanelSkeleton, MiniCalendarSkeleton, OverrideTogglesSkeleton, CycleInfoSkeleton, DashboardSkeleton), 29 tests in skeletons.test.tsx; loading.tsx files for all routes (dashboard, calendar, history, plan, settings); shimmer animations matching spec requirements - [x] **P3.8: Loading States** - Complete with skeleton components (DecisionCardSkeleton, DataPanelSkeleton, NutritionPanelSkeleton, MiniCalendarSkeleton, OverrideTogglesSkeleton, CycleInfoSkeleton, DashboardSkeleton), 29 tests in skeletons.test.tsx; loading.tsx files for all routes (dashboard, calendar, history, plan, settings); shimmer animations matching spec requirements
- [x] **P3.9: Token Expiration Warnings** - Complete with 10 new tests in email.test.ts, 10 new tests in garmin-sync/route.test.ts; sends warnings at 14 and 7 days before expiry - [x] **P3.9: Token Expiration Warnings** - Complete with 10 new tests in email.test.ts, 10 new tests in garmin-sync/route.test.ts; sends warnings at 14 and 7 days before expiry
- [x] **P3.11: Missing Component Tests** - Complete with 82 tests across 5 component test files (DecisionCard: 11, DataPanel: 18, NutritionPanel: 12, OverrideToggles: 18, DayCell: 23) - [x] **P3.11: Missing Component Tests** - Complete with 90 tests across 5 component test files (DecisionCard: 19, DataPanel: 18, NutritionPanel: 12, OverrideToggles: 18, DayCell: 23)
### P4: UX Polish and Accessibility ### P4: UX Polish and Accessibility
- [x] **P4.1: Dashboard Onboarding Banners** - Complete with OnboardingBanner component (16 tests), dashboard integration (5 new tests) - [x] **P4.1: Dashboard Onboarding Banners** - Complete with OnboardingBanner component (16 tests), dashboard integration (5 new tests)
@@ -977,7 +977,7 @@ Analysis of all specs vs implementation revealed these gaps:
| Garmin sync structured logging | observability.md | **COMPLETE** | Added sync start/complete/failure logging | | Garmin sync structured logging | observability.md | **COMPLETE** | Added sync start/complete/failure logging |
| Email sent/failed logging | observability.md | **COMPLETE** | Email events now logged (info for success, error for failure) with structured data (userId, emailType, success) | | Email sent/failed logging | observability.md | **COMPLETE** | Email events now logged (info for success, error for failure) with structured data (userId, emailType, success) |
| Period history UI | cycle-tracking.md | **PENDING** | UI for viewing/editing past periods | | Period history UI | cycle-tracking.md | **PENDING** | UI for viewing/editing past periods |
| Dashboard color-coded backgrounds | dashboard.md | **PENDING** | Phase-based background colors | | Dashboard color-coded backgrounds | dashboard.md | **COMPLETE** | DecisionCard shows RED/YELLOW/GREEN backgrounds per status (8 new tests) |
| Toast notifications | dashboard.md | **PENDING** | Success/error toasts for user actions | | Toast notifications | dashboard.md | **PENDING** | Success/error toasts for user actions |
| CI pipeline | testing.md | **PENDING** | GitHub Actions for test/lint/build | | CI pipeline | testing.md | **PENDING** | GitHub Actions for test/lint/build |

View File

@@ -122,6 +122,112 @@ describe("DecisionCard", () => {
}); });
}); });
describe("color-coded backgrounds", () => {
it("renders REST with red background", () => {
const decision: Decision = {
status: "REST",
reason: "Recovery day",
icon: "🛌",
};
const { container } = render(<DecisionCard decision={decision} />);
const card = container.firstChild as HTMLElement;
expect(card).toHaveClass("bg-red-100", "border-red-300");
});
it("renders GENTLE with yellow background", () => {
const decision: Decision = {
status: "GENTLE",
reason: "Light movement",
icon: "🧘",
};
const { container } = render(<DecisionCard decision={decision} />);
const card = container.firstChild as HTMLElement;
expect(card).toHaveClass("bg-yellow-100", "border-yellow-300");
});
it("renders LIGHT with yellow background", () => {
const decision: Decision = {
status: "LIGHT",
reason: "Moderate intensity",
icon: "🚶",
};
const { container } = render(<DecisionCard decision={decision} />);
const card = container.firstChild as HTMLElement;
expect(card).toHaveClass("bg-yellow-100", "border-yellow-300");
});
it("renders REDUCED with yellow background", () => {
const decision: Decision = {
status: "REDUCED",
reason: "Lower intensity",
icon: "⬇️",
};
const { container } = render(<DecisionCard decision={decision} />);
const card = container.firstChild as HTMLElement;
expect(card).toHaveClass("bg-yellow-100", "border-yellow-300");
});
it("renders TRAIN with green background", () => {
const decision: Decision = {
status: "TRAIN",
reason: "Full intensity approved",
icon: "🏃",
};
const { container } = render(<DecisionCard decision={decision} />);
const card = container.firstChild as HTMLElement;
expect(card).toHaveClass("bg-green-100", "border-green-300");
});
it("renders reason with appropriate text color for REST", () => {
const decision: Decision = {
status: "REST",
reason: "Rest message",
icon: "🛌",
};
render(<DecisionCard decision={decision} />);
const reason = screen.getByText("Rest message");
expect(reason).toHaveClass("text-red-700");
});
it("renders reason with appropriate text color for GENTLE", () => {
const decision: Decision = {
status: "GENTLE",
reason: "Gentle message",
icon: "🧘",
};
render(<DecisionCard decision={decision} />);
const reason = screen.getByText("Gentle message");
expect(reason).toHaveClass("text-yellow-700");
});
it("renders reason with appropriate text color for TRAIN", () => {
const decision: Decision = {
status: "TRAIN",
reason: "Train message",
icon: "🏃",
};
render(<DecisionCard decision={decision} />);
const reason = screen.getByText("Train message");
expect(reason).toHaveClass("text-green-700");
});
});
describe("styling", () => { describe("styling", () => {
it("renders within a bordered container", () => { it("renders within a bordered container", () => {
const decision: Decision = { const decision: Decision = {
@@ -133,7 +239,7 @@ describe("DecisionCard", () => {
const { container } = render(<DecisionCard decision={decision} />); const { container } = render(<DecisionCard decision={decision} />);
const card = container.firstChild as HTMLElement; const card = container.firstChild as HTMLElement;
expect(card).toHaveClass("rounded-lg", "border", "p-6"); expect(card).toHaveClass("rounded-lg", "p-6");
}); });
it("renders status as bold heading", () => { it("renders status as bold heading", () => {
@@ -150,17 +256,17 @@ describe("DecisionCard", () => {
expect(heading).toHaveClass("font-bold"); expect(heading).toHaveClass("font-bold");
}); });
it("renders reason with muted color", () => { it("renders reason with status-specific color", () => {
const decision: Decision = { const decision: Decision = {
status: "REST", status: "REST",
reason: "Muted reason text", reason: "Reason with status color",
icon: "🛌", icon: "🛌",
}; };
render(<DecisionCard decision={decision} />); render(<DecisionCard decision={decision} />);
const reason = screen.getByText("Muted reason text"); const reason = screen.getByText("Reason with status color");
expect(reason).toHaveClass("text-gray-600"); expect(reason).toHaveClass("text-red-700");
}); });
}); });
}); });

View File

@@ -1,17 +1,43 @@
// ABOUTME: Dashboard card displaying today's training decision. // ABOUTME: Dashboard card displaying today's training decision.
// ABOUTME: Shows decision status, icon, and reason prominently. // ABOUTME: Shows decision status, icon, and reason with color-coded backgrounds.
import type { Decision } from "@/types"; import type { Decision } from "@/types";
interface DecisionCardProps { interface DecisionCardProps {
decision: Decision; decision: Decision;
} }
function getStatusColors(status: Decision["status"]): {
background: string;
text: string;
} {
switch (status) {
case "REST":
return { background: "bg-red-100 border-red-300", text: "text-red-700" };
case "GENTLE":
case "LIGHT":
case "REDUCED":
return {
background: "bg-yellow-100 border-yellow-300",
text: "text-yellow-700",
};
case "TRAIN":
return {
background: "bg-green-100 border-green-300",
text: "text-green-700",
};
default:
return { background: "border", text: "text-gray-600" };
}
}
export function DecisionCard({ decision }: DecisionCardProps) { export function DecisionCard({ decision }: DecisionCardProps) {
const colors = getStatusColors(decision.status);
return ( return (
<div className="rounded-lg border p-6"> <div className={`rounded-lg p-6 ${colors.background}`}>
<div className="text-4xl mb-2">{decision.icon}</div> <div className="text-4xl mb-2">{decision.icon}</div>
<h2 className="text-2xl font-bold">{decision.status}</h2> <h2 className="text-2xl font-bold">{decision.status}</h2>
<p className="text-gray-600">{decision.reason}</p> <p className={colors.text}>{decision.reason}</p>
</div> </div>
); );
} }