Imported from Zhadowseb/FitVen (
src/AGENTS.md). Install upstream withnpx skills add Zhadowseb/FitVen --skill src. Copyright stays with the author.
AGENTS.md
Scope
This file applies to everything inside src/.
Source Layout
Pages/: screen-level UI, local components, and stylesDatabase/: SQLite schema, database setup, and Supabase wiringRepository/: data access functions close to persistence concernsServices/: business logic and multi-step app flowsContexts/: React context and app-level stateLocalization/: the translations (locales/en,locales/da), thet()function and the provider behinduseTranslation()Resources/: shared UI components, theme, icons, and design primitivesSync/: sync-related flowsUtils/: reusable helpers with minimal side effects
Structure Rules
- Keep logic close to the feature that owns it before extracting shared abstractions.
- Prefer the existing layer boundaries over bypassing them with shortcut imports or direct database calls from screens.
- When moving or renaming files, update imports in the same change.
- Reuse
Resourcesand existing services/repositories before creating parallel patterns.
Sync Rules
- Treat
Setas the lowest-level cloud sync boundary for strength workout data. - Keep
Workout_Type_Instancecloud sync focused on workout-level fields such as workout type, label, date, completion, and timer state. - Changes to
Exercise_Instanceshould update local state immediately and sync the owning exercise row without bypassing the established repository and service layers. - Changes to
Setrows should update local state immediately, keep the owningExercise_Instancederived fields in sync locally, and then sync both levels in the correct parent-first order. - Keep
Rundata on its own workout-level sync path until a lower-level running sync exists.
Blocking
- Everything one user can see about another is gated on a row in
public.user_follows. A block deletes those rows in both directions and a trigger refuses new ones, so activity, posts, likes and push notifications all stop without any of their own policies knowing blocking exists. Keep it that way: a new follower-visible feature inherits the block for free as long as it reads throughuser_follows. - Never check a block in an RLS policy expression. The policy runs as the
person doing the write, and the block row they need to see belongs to
somebody else, so the check silently passes. It has to be a
security definertrigger or function. public.profilesno longer answers for strangers. Finding someone you have no relationship with goes throughpublic.search_profiles, and the list of people you blocked throughpublic.list_blocked_profiles— bothsecurity definer, both filtering blocks in either direction. Somebody's profile page reads throughpublic.public_profilethe same way: a fixed set of fields, nothing fromprofile_private, and null for a block either way.
Conventions The Code Depends On
- Every service and repository function takes
dbas its first argument. The screen gets it withuseSQLiteContext()and passes it down. - Import through the barrel:
from "@services", notfrom "@services/programService". Same for@resources/ThemedComponents. - Use a path alias instead of climbing out of the folder. The seven are
@contexts,@database,@localization,@repository,@resources,@servicesand@utils, defined inbabel.config.jsand mirrored intsconfig.jsonso editor navigation follows../and../stay for a file's own neighbours. The 149 older deep imports were deliberately left alone; convert one when a change is already touching its file. - Never alias one layer to another layer's name. 45 function names exist in
both
ServicesandRepositorywith the same signature, soimport { xService as xRepository }sends the next reader to the wrong file.npm testfails if one reappears. - Folders and component files are PascalCase. Service, repository and utils files are camelCase.
Utils/is for helpers with minimal side effects. A helper that opens the database belongs in a service.
Text The User Reads
- Every string a user sees goes through
Localization/: in a componentconst { t } = useTranslation()from@localization, anywhere elseimport { t } from "@localization". A literal English string in JSX is a string Danish users read in English. - Keys are
<area>.<name>, one file per area underlocales/enandlocales/da, and the two files carry the same keys.npm testrunsscripts/test-localization.js, which fails on a key in one language and not the other, and on at("...")insrc/whose key exists in neither. - Plurals are an object with
oneandother(andzerowhen it reads differently), chosen bycount:t("common.sets", { count }). - A label computed at module load (a constant array of options, say) is
frozen in whatever language the app started in. Build it inside the
component, or make it a function of
t. - Dates and numbers go through
formatDate,formatTimeandformatNumberfrom@localization, which use the chosen language's locale rather than the device's. - Workout types are stored in English (
Resistance,Upperbody,Run,Walk...), and a workout nobody named has its type as itslabel. Draw a type withworkoutTypeLabeland a workout's name withworkoutDisplayName(label, t, workoutType)fromsrc/Utils/workoutTypeLabel.js; never renderlabelorworkout_typeas it is, and pass the row's type - it is what tells the type fallback from a typed "Run". Only what is drawn changes - a name the user typed stays as typed, and nothing stored is translated. The one case it cannot see: the names the app gives a strength workout after its exercises ("Push", "Legs") are stored like typed ones, so "Legs" typed on one reads "Ben".
What "Exercise" Means
Five names, and they are not interchangeable:
| Name | What it is |
|---|---|
table Exercise |
the catalog of exercise names. Was called Exercise_storage; db.js still handles the rename for old installs, which is why getExerciseStorage has that name. |
table Exercise_Instance |
one exercise inside one concrete workout. Unrelated to the catalog. |
ExerciseCatalogPage |
the screen that shows and picks from the catalog |
ExerciseLibraryPage |
the Train tab: the active program or your split, then your library - calendar, workouts, records, exercises, programs, the 1RM calculator and sick days |
ExerciseLibraryList |
the list itself. It lives under ExerciseLibraryPage but both screens use it. |
Surprising Placements
- The global bottom navigation and the whole start-a-workout flow live in
src/Resources/ThemedComponents/ThemedBottomNavigation.jsandsrc/Resources/Components/StartWorkoutSheet.js— not insrc/Pages/. The navigation is mounted inApp.jsoutside the navigator. - The run screen is split, but only its pure parts.
Run.jsis still ~4,500 lines because the GPS and Bluetooth hooks are wired into its state and cannot move without a device to test on. The maths that could move lives beside it inrunDisplayUtils.js(sections, route, charts),runFormatUtils.js(pace, clock, distance),runEnduranceStats.jsandrunFlowOptions.js, and is the only part with tests. Put new run maths there, not back in the screen. - The lock-screen card during a strength workout is native, outside
src/:modules/live-workoutandtargets/widgets. What it shows is decided inUtils/liveWorkout.jsand mirrored in Swift and Kotlin; seemodules/live-workout/AGENTS.mdbefore changing either. src/Pages/WeekPage/is outside the active user flow, but it is still a registered route.
Related Guides
- See
src/Pages/AGENTS.mdfor UI-specific guidance. - See
src/Database/AGENTS.mdfor schema and persistence guidance. - See
src/Services/AGENTS.mdfor the cloud sync field checklist. - See
src/Sync/AGENTS.mdfor which sync components actually run.
