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:
Trey t
2026-02-22 20:07:40 -06:00
parent 2a16b75c4b
commit 1c61b80731
111 changed files with 28108 additions and 30 deletions

View File

@@ -0,0 +1,137 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
const tabs = [
{
href: "/dashboard",
label: "Dashboard",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
</svg>
),
},
{
href: "/plans",
label: "Plans",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
),
},
{
href: "/preferences",
label: "Prefs",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
),
},
{
href: "/rules",
label: "Rules",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
),
},
{
href: "/history",
label: "History",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
),
},
];
export function BottomNav() {
const pathname = usePathname();
return (
<nav className="fixed bottom-0 left-0 right-0 z-50 h-16 pb-safe bg-zinc-900 border-t border-zinc-800 md:hidden">
<div className="flex items-center justify-around h-full">
{tabs.map((tab) => {
const isActive =
pathname === tab.href || pathname.startsWith(tab.href + "/");
return (
<Link
key={tab.href}
href={tab.href}
className={`flex flex-col items-center gap-1 text-xs font-medium transition-colors duration-150 ${
isActive ? "text-[#39FF14]" : "text-zinc-500"
}`}
>
{tab.icon}
<span>{tab.label}</span>
</Link>
);
})}
</div>
</nav>
);
}

View File

@@ -0,0 +1,73 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useAuth } from "@/lib/auth";
import { Button } from "@/components/ui/Button";
const navLinks = [
{ href: "/dashboard", label: "Dashboard" },
{ href: "/plans", label: "Plans" },
{ href: "/rules", label: "Rules" },
{ href: "/history", label: "History" },
];
export function Navbar() {
const pathname = usePathname();
const router = useRouter();
const { user, logout } = useAuth();
const handleLogout = () => {
logout();
router.push("/login");
};
return (
<nav className="fixed top-0 left-0 right-0 z-50 h-16 bg-zinc-900/80 backdrop-blur border-b border-zinc-800 px-6 flex items-center justify-between">
<Link href="/dashboard" className="text-[#39FF14] font-bold text-xl tracking-tight">
WERKOUT
</Link>
<div className="hidden md:flex items-center gap-6">
{navLinks.map((link) => {
const isActive =
pathname === link.href || pathname.startsWith(link.href + "/");
return (
<Link
key={link.href}
href={link.href}
className={`text-sm font-medium transition-colors duration-150 ${
isActive
? "text-[#39FF14]"
: "text-zinc-400 hover:text-zinc-100"
}`}
>
{link.label}
</Link>
);
})}
</div>
<div className="flex items-center gap-3">
{user && (
<span className="text-sm text-zinc-300 hidden sm:inline">
{user.first_name}
</span>
)}
<Link
href="/preferences"
className={`text-sm font-medium transition-colors duration-150 ${
pathname === "/preferences"
? "text-[#39FF14]"
: "text-zinc-400 hover:text-zinc-100"
}`}
>
Preferences
</Link>
<Button variant="ghost" size="sm" onClick={handleLogout}>
Logout
</Button>
</div>
</nav>
);
}