FitAI: The Fitness App That Turns Workout Logs Into a Controlled AI Decision System

It does more than track sets and streaks. It builds a performance brief, protects workout plans from model drift, and uses cache-aware AI to answer training questions with structure instead of guesswork.

8 min read • View on GitHub • More from Arjun8242

A pile of raw workout logs and handwritten set counts passes through a narrow sorting gate and emerges as a clean performance dossier, while a locked planning door stays closed to an outstretched mechanical hand. The scene explains that FitAI transforms messy logs into constrained analysis instead of letting the model touch the plan directly.
FitAI treats workout history like input to a decision system, not decoration for a chatbot.
Key Takeaways

Why FitAI feels more like a fitness analyst than a tracker

Most fitness apps optimize for logging. FitAI optimizes for judgment. It takes the last 30 days of workout history, turns them into a structured performance brief, and only then asks an LLM to help interpret the result.

That sounds subtle, but it changes the product. The model is not the system. It is a constrained component inside a workflow that already knows how to classify intent, synthesize context, and protect workout plans from direct mutation.

The result is a fitness app that behaves less like a chat window with dumbbells and more like an analytics engine with an AI front end.

The router at the front door

The first important move is classification. Before anything reaches Gemini, FitAI asks what kind of request this is: a direct database lookup, a performance analysis request, or general knowledge.

FitAI separates questions by intent before any expensive reasoning happens. The router decides whether to hit the database, synthesize workout context, or answer general training questions.

function classifyIntent(message) {
  const text = message.toLowerCase();
  if (/streak|workout log|last workout|today.*workout/.test(text)) return 'direct';
  if (/plateau|progress|balance|volume|why am i/.test(text)) return 'performance';
  return 'knowledge';
}

async function handleAIRequest(message, userId) {
  const intent = classifyIntent(message);
  if (intent === 'direct') return getDirectData(userId, message);
  if (intent === 'performance') return analyzePerformance(userId);
  return askGemini(message);
}

That router matters because it prevents unnecessary model calls. A streak question does not need a narrative answer. A plateau question does. A generic training concept does not need to be wrapped in workout history at all.

This is the first sign that FitAI is organized around workflow, not novelty.

How FitAI turns logs into a performance brief

The core of the repo lives in the context aggregator. It does the boring work that makes the AI useful: it gathers recent sessions, calculates volume trends, identifies weak muscle groups, and tracks how recently the user hit a personal record.

A close-up scale balances recent training sessions against different muscle groups. Primary muscles carry heavier weights than secondary muscles, and one muscle group dips below a threshold flag. The image explains how FitAI computes a weighted performance brief instead of treating every set as equal.
FitAI gives primary and secondary muscles different credit, then uses that weighting to flag imbalance.

The clever part is not that it summarizes data. It is how it summarizes data. FitAI compares recent training blocks, weighs muscle groups differently, and surfaces the kinds of signals a coach would actually use.

Primary muscles get full credit. Secondary muscles get partial credit. That makes the weak-group analysis more faithful to real training than a naive set counter ever could.

It also explains why the output feels specific. The model is not hallucinating structure. The structure already exists before the prompt is assembled.

const PRIMARY_WEIGHT = 1.0;
const SECONDARY_WEIGHT = 0.5;

function weightedSets(session) {
  return session.exercises.reduce((total, exercise) => {
    const weight = exercise.muscleRole === 'primary'
      ? PRIMARY_WEIGHT
      : SECONDARY_WEIGHT;
    return total + (exercise.sets * weight);
  }, 0);
}

function flagWeakMuscles(muscleTotals) {
  const average = Object.values(muscleTotals).reduce((a, b) => a + b, 0) / Object.keys(muscleTotals).length;
  return Object.entries(muscleTotals)
    .filter(([, total]) => total < average * 0.6)
    .map(([muscle]) => muscle);
}

The analysis cache is part of the product, not just the backend

FitAI caches performance analysis for 24 hours unless new data arrives. That keeps the expensive reasoning path fast and keeps API usage under control without sacrificing freshness when the user actually trains again.

This is a practical design choice, but it is also an editorial one. It tells you what the app believes is worth recomputing. A new workout matters. Reasking the same question does not.

Why the streak logic understands rest days

A lot of fitness apps turn streaks into a blunt instrument. Miss a day and the count breaks. FitAI is smarter. It checks whether the missed day was even supposed to be a workout day before deciding the streak is over.

SituationBasic trackerFitAI
User skips TuesdayStreak breaksStreak can remain intact if Tuesday was not a planned workout day
User trains on scheduleCount increasesCount increases
User misses a planned sessionStreak breaksStreak breaks
User asks about progressRaw logs onlyContextualized against recent volume and weak-group signals

That matters because it aligns the app with how real training works. Rest is not failure. In many programs, rest is part of the plan.

A streak system that understands that distinction feels less gamified and more coached.

The guardrails that keep the AI from becoming dangerous

FitAI is careful about what the model can do. The system prompt keeps the assistant advisory only, and the plan builder remains outside the LLM's direct control. That separation is the difference between a helpful assistant and an unstable one.

The backend also does unglamorous but important work: timeout handling, manual JSON extraction from model output, and explicit error paths when the response is malformed. Those details matter because LLMs are useful until they are not.

By treating the model as a fallible component, the repo avoids a common trap. It does not pretend the model is a database, a validator, or a source of truth.

const SYSTEM_PROMPT = `You are advisory only and cannot directly modify workout plans.\nUse performance context when available.\nIf the user needs plan changes, direct them to the Plan Builder.`;

async function analyzePerformance(userId) {
  const cached = await getCachedAnalysis(userId);
  if (cached && !hasNewWorkoutData(userId)) return cached;

  const context = await buildPerformanceContext(userId);
  const raw = await callGemini({ system: SYSTEM_PROMPT, context });
  const json = extractJSON(raw);
  await saveCachedAnalysis(userId, json, 24 * 60 * 60);
  return json;
}

The JSON extraction step is especially telling. It is a small admission that the model may return messy output. FitAI still ships because the code expects that mess and cleans it up.

Why the stack matters

The stack is modern, but the point is not fashion. The backend uses Express, Mongoose, JWT cookies, Joi validation, and Docker. The frontend uses React, Vite, Tailwind, Framer Motion, and Recharts. Together, they support a product that needs routing, state, visualization, and controlled AI calls.

LayerWhat it doesWhy it fits FitAI
React and ViteFast client UISupports quick interaction around analysis and logs
Express and MongooseAPI and data accessKeeps routing and aggregation close to the data
Gemini integrationNarrative analysisUsed only after context is structured
Docker and CIRepeatable deploymentUseful for a system with backend, frontend, and MongoDB

This is a monorepo with a clear division of labor. That structure matches the product's thesis: gather data, shape it, then ask the model to explain it.

What FitAI does better than a normal tracker

FitAI is not trying to out-chat a chatbot, and it is not trying to out-log a tracker. It wins by combining domain logic with AI in the right order.

CapabilityBasic trackerGeneric AI assistantFitAI
Understands workout contextLimitedNoYes
Protects workout plans from direct model mutationNoNoYes
Caches expensive analysisSometimesRarelyYes
Handles rest days intelligentlyRarelyNoYes
Computes muscle balanceNoNoYes
Gives direct database answers when appropriateNoNoYes

That is the real differentiator. FitAI uses AI where reasoning helps, and it uses deterministic code where correctness matters. The model stays useful because it stays inside a system that already knows what good fitness data looks like.

For anyone building AI products, that is the lesson worth stealing.