workout generator audit: rules engine, structure rules, split patterns, injury UX, metadata cleanup
- Add rules_engine.py with quantitative rules for all 8 workout types - Add quality gate retry loop in generate_single_workout() - Expand calibrate_structure_rules to all 120 combinations (8 types × 5 goals × 3 sections) - Wire WeeklySplitPattern DB records into _pick_weekly_split() - Enforce movement patterns from WorkoutStructureRule in exercise selection - Add straight-set strength support (single main lift, 4-6 rounds) - Add modality consistency check for duration-dominant workout types - Add InjuryStep component to onboarding and preferences - Add sibling exercise exclusion in regenerate and preview_day endpoints - Display generator warnings on dashboard - Expand fix_rep_durations, fix_exercise_flags, fix_movement_pattern_typo - Add audit_exercise_data and check_rules_drift management commands - Add Next.js frontend with dashboard, onboarding, preferences, history pages - Add generator app with ML-powered workout generation pipeline - 96 new tests across 7 test modules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
246
werkout-frontend/app/dashboard/page.tsx
Normal file
246
werkout-frontend/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AuthGuard } from "@/components/auth/AuthGuard";
|
||||
import { Navbar } from "@/components/layout/Navbar";
|
||||
import { BottomNav } from "@/components/layout/BottomNav";
|
||||
import { WeeklyPlanGrid } from "@/components/plans/WeeklyPlanGrid";
|
||||
import { WeekPicker } from "@/components/plans/WeekPicker";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { api } from "@/lib/api";
|
||||
import type { GeneratedWeeklyPlan, WeeklyPreview } from "@/lib/types";
|
||||
|
||||
function getCurrentMonday(): string {
|
||||
const now = new Date();
|
||||
const day = now.getDay();
|
||||
const diff = day === 0 ? -6 : 1 - day;
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() + diff);
|
||||
const yyyy = monday.getFullYear();
|
||||
const mm = String(monday.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(monday.getDate()).padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const router = useRouter();
|
||||
const [selectedMonday, setSelectedMonday] = useState(getCurrentMonday);
|
||||
const [plans, setPlans] = useState<GeneratedWeeklyPlan[]>([]);
|
||||
const [preview, setPreview] = useState<WeeklyPreview | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const fetchPlans = useCallback(async () => {
|
||||
try {
|
||||
try {
|
||||
const prefs = await api.getPreferences();
|
||||
const hasPrefs =
|
||||
prefs.available_equipment.length > 0 ||
|
||||
prefs.preferred_workout_types.length > 0 ||
|
||||
prefs.target_muscle_groups.length > 0;
|
||||
if (!hasPrefs) {
|
||||
router.replace("/onboarding");
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
router.replace("/onboarding");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await api.getPlans();
|
||||
setPlans(data);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch plans:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlans();
|
||||
}, [fetchPlans]);
|
||||
|
||||
// Clear preview when week changes
|
||||
useEffect(() => {
|
||||
setPreview(null);
|
||||
}, [selectedMonday]);
|
||||
|
||||
const savedPlan = plans.find((p) => p.week_start_date === selectedMonday);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setGenerating(true);
|
||||
setError("");
|
||||
try {
|
||||
const data = await api.previewPlan(selectedMonday);
|
||||
setPreview(data);
|
||||
} catch (err) {
|
||||
const msg =
|
||||
err instanceof Error ? err.message : "Failed to generate preview";
|
||||
setError(msg);
|
||||
console.error("Failed to generate preview:", err);
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!savedPlan) return;
|
||||
setConfirming(true);
|
||||
setError("");
|
||||
try {
|
||||
await api.confirmPlan(savedPlan.id);
|
||||
await fetchPlans();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "Failed to confirm plan";
|
||||
setError(msg);
|
||||
} finally {
|
||||
setConfirming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!preview) return;
|
||||
setSaving(true);
|
||||
setError("");
|
||||
try {
|
||||
await api.savePlan(preview);
|
||||
setPreview(null);
|
||||
await fetchPlans();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "Failed to save plan";
|
||||
setError(msg);
|
||||
console.error("Failed to save plan:", err);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthGuard>
|
||||
<Navbar />
|
||||
<BottomNav />
|
||||
<main className="pt-20 pb-20 px-4 max-w-5xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold text-zinc-100">Dashboard</h1>
|
||||
<WeekPicker
|
||||
selectedMonday={selectedMonday}
|
||||
onChange={setSelectedMonday}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{preview?.warnings && preview.warnings.length > 0 && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20 text-yellow-300 text-sm">
|
||||
<div className="font-medium mb-1">Heads up</div>
|
||||
<ul className="list-disc list-inside space-y-0.5">
|
||||
{preview.warnings.map((w, i) => (
|
||||
<li key={i}>{w}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
) : preview ? (
|
||||
/* ===== Preview mode ===== */
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-zinc-200">
|
||||
Preview
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setPreview(null)}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
loading={generating}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
Regenerate
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
loading={saving}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Plan
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<WeeklyPlanGrid
|
||||
preview={preview}
|
||||
onPreviewChange={setPreview}
|
||||
/>
|
||||
</div>
|
||||
) : savedPlan ? (
|
||||
/* ===== Saved plan mode ===== */
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-zinc-200">
|
||||
This Week's Plan
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
{savedPlan.generated_workouts.some(
|
||||
(w) => !w.is_rest_day && w.status === "pending"
|
||||
) && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
loading={confirming}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
Save to Calendar
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
loading={generating}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
Regenerate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<WeeklyPlanGrid plan={savedPlan} onUpdate={fetchPlans} />
|
||||
</div>
|
||||
) : (
|
||||
/* ===== No plan ===== */
|
||||
<div className="flex flex-col items-center justify-center py-20 gap-6">
|
||||
<p className="text-zinc-400 text-lg text-center">
|
||||
No plan for this week yet. Let's get started!
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
loading={generating}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
Generate Plan
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user