Appearance
Exercise library
Exercise Library combines third-party provider content with coach-owned custom exercises.
Tags: Provider adapter, Coach-owned custom exercises, Program-builder search, Thumbnail mappings, Video URL validation
Area: ProgramsAndExercise / Provider And Coach-Owned Catalog / Focused Search
The V1 baseline should not manually maintain a full exercise library, but coaches must be able to create their own exercises when provider content is missing or they want to use their own demonstration. CoachMe integrates with a selected external exercise content provider, normalizes provider-specific values into stable internal catalog and taxonomy records, supports coach-created exercises with uploaded videos, uploaded thumbnails, or validated video URL references, uses reviewed thumbnails from allowed sources, supports Arabic search fallback, and snapshots selected exercises into program drafts so historical programs remain understandable if provider or coach-owned content changes.
Ownership boundary
V1 baseline decision: the Exercise Library owns provider configuration, catalog cache, coach-owned exercise lifecycle, exercise media assets, external video URL validation, taxonomy cache, translation terms, search behavior, and provider sync health. Program Builder owns program structure and selected exercise configuration, but it stores an immutable exercise snapshot supplied by this library when a coach adds an exercise.
V1 baseline provider
V1 baseline provider decision: start with YMove using one global provider configuration. Before production launch, re-check YMove terms, pricing, usage limits, media rules, and regional availability against the chosen subscription.
Provider-neutral contracts
Provider-specific details such as exact endpoints, vendor quotas, media URL rules, third-party thumbnail sources, and import scripts belong in separate integration notes. This page defines the provider-neutral Exercise Library contracts around the initial YMove adapter.
Exercise Library Flows
The library is mostly an infrastructure-backed domain service. Coaches experience it through program builder search, muscle browsing, and exercise selection.
Provider Bootstrap & Sync
- System admin configures YMove as the selected global provider, credentials reference, enabled locales, provider capabilities, current plan limits, and licensing flags.
- Bootstrap job fetches muscle groups, equipment, movement patterns, exercise types, and exercise metadata according to the provider's safe caching rules.
- Exercise type values are stored as
ExerciseTaxonomyNoderecords withtaxonomy_type = exercise_type; provider-specific values are preserved and reviewed instead of flattened into an application enum. - Provider data is normalized into internal catalog items, taxonomy nodes, media refs, and selection snapshots keyed by provider id and content hash.
- Thumbnail import and mapping jobs connect approved image assets to provider catalog items after automated scoring and manual review where needed.
- Catalog and taxonomy updates emit events so web and coach mobile can invalidate cached filter lists.
- Provider health is monitored through sync runs, latency metrics, quota errors, and stale-cache age.
Coach Adds Exercise
- Coach opens a program draft and selects the active training day or workout block.
- Program Builder requests exercise taxonomy and shows provider muscle groups on the same screen as the builder.
- Coach chooses a muscle group or starts typing. Typing enters the focused search overlay state.
- Search endpoint reads the local normalized catalog and returns approved local thumbnails or placeholders.
- Coach selects an exercise. The API fetches or verifies details, returns a safe selection snapshot, and Program Builder inserts it into the active day or group.
- Coach configures sets, reps, RPE/RIR, rest, notes, and approved alternatives in Program Builder.
Coach Creates Custom Exercise
- Coach opens Exercise Library from Program Builder or the library management entry point and selects create exercise.
- Coach enters name, exercise type taxonomy selection, instructions, cues, primary muscles, equipment, difficulty, and optional Arabic labels.
- Coach chooses media source: upload a video, upload a thumbnail, provide an external video URL reference, or save without media.
- Uploaded video and thumbnail files go through signed upload, malware scan, MIME validation, processing, and moderation status checks before becoming active.
- External video URLs are saved only after backend validation confirms the URL is safe, reachable, and renderable by the supported player or embed strategy.
- When active, the custom exercise appears in search with
COACH_CUSTOMsource metadata and can be selected into program drafts through the same snapshot contract as provider exercises.
Source Filtering & UI Separation
- Exercise search defaults to all selectable sources, but exposes clear filters for My Library and Provider Library.
- Search result cards show a persistent source badge, such as My Exercise or the third-party provider name.
- My Library results include coach-owned uploaded thumbnail or custom placeholder styling; provider results use approved mapped thumbnails or provider-safe placeholders.
- The UI must not visually blend coach-created exercises with third-party provider content without source labels, because ownership affects editing, licensing, and media behavior.
Arabic Search Fallback
- Search detects query script and requested locale.
- The selected provider is treated as not supporting Arabic search or Arabic content unless its adapter explicitly declares that support.
- The query normalizer maps Arabic terms to English exercise names and taxonomy nodes, including exercise type, muscle, equipment, and movement synonyms before searching the local catalog.
- Results are matched with inferred English terms and displayed with Arabic labels where local reviewed terms exist.
- Unknown or low-confidence mappings create review tasks for the content/admin backlog.
Provider Degradation
- When provider sync or detail refresh is slow or unavailable, the API can serve local catalog results marked with a degraded-source flag.
- Exercise selection requires a provider-specific recent detail snapshot, with a default maximum stale age of 14 days unless YMove or a future provider requires a shorter TTL.
- If cached provider details are older than the allowed TTL, the backend must verify or refresh the exercise before creating a selection snapshot; failed verification blocks new selection.
- Existing program drafts and assigned programs continue rendering from stored exercise snapshots.
- New searches surface retry and provider-status errors without breaking the surrounding program draft.
Domain Model
Exercise Library belongs to ProgramsAndExercise and exposes catalog capabilities to Program Builder through application services and typed DTOs.
Aggregates & Entities
ExerciseProviderIntegration: configured provider, capabilities, credential reference, enabled locales, sync policy, plan limits, and licensing constraints.ExerciseCatalogItem: normalized selectable exercise with stable internal id, source type, provider id or coach owner id, names, taxonomy node references, media references, and availability status.ExerciseTaxonomyNode: normalized muscle group, equipment, movement pattern, exercise type, difficulty, or modality value from the selected provider or CoachMe-reviewed taxonomy.ExerciseThumbnailMapping: reviewed mapping from a provider catalog item to a local or licensed image asset.CoachExerciseMediaAsset: coach-uploaded video or thumbnail asset tied to one custom exercise and stored through CoachMe-controlled object storage.ExerciseExternalVideoRef: externally hosted video URL reference with validation status, render strategy, and last validation result.ExerciseTranslationTerm: reviewed local synonym or display label used for Arabic fallback and localized browsing.ExerciseProviderSyncRun: append-only record of provider bootstrap, incremental sync, backfill, or health-check execution.ExerciseMediaSession: short-lived backend playback session that resolves provider or custom media, enforces usage limits, and records access without exposing durable provider URLs to clients.ExerciseMediaAccessEvent: append-only record used for media access plan enforcement.ExerciseSelectionSnapshot: immutable value object handed to Program Builder when an exercise is added to a program day or block.
Value Objects & Rules
ProviderKey,ProviderExerciseId,ProviderTaxonomyId: provider identities are never guessed or rewritten.ProviderKeyis an opaque provider identity selected through configuration, with room for future provider replacement.ExerciseCatalogSource:PROVIDERorCOACH_CUSTOM; every search result and snapshot carries this source explicitly.ExerciseTaxonomyType: taxonomy category used by search, custom exercise forms, provider mapping, and snapshots:muscle_group,equipment,movement_pattern,exercise_type,difficulty, andmodality.ExerciseSearchFilters: query, locale, exercise type taxonomy filter, muscle group, equipment, movement pattern, difficulty, source filter, owner filter, and pagination cursor.ExerciseMediaRef: backend-resolved provider media reference, CoachMe object-storage asset reference, external URL render reference, duration, and licensing flags. Provider playback goes through a CoachMe media-session endpoint so usage can be tracked and limited.ExternalVideoValidationResult: URL scheme, final URL after redirects, provider kind, HTTP status, content type, oEmbed/embed support, player render strategy, validation status, and failure reason.ExerciseCapabilitySet: search, details, taxonomy, media, Arabic search, Arabic content, and webhook support.- Provider-specific response shapes must terminate at the adapter boundary. Application services consume only normalized DTOs and internal catalog entities.
- Only active catalog items can be inserted into new program drafts. Deprecated provider exercises render only from existing program snapshots.
- Selected exercises store snapshot fields needed for program rendering: names, catalog source, exercise type taxonomy labels, provider or custom owner reference, muscles, equipment, media refs, and movement metadata.
- Provider content is not edited by coaches in Public V1. Coach-created custom exercises default to workspace visibility so the coaching team can share them later; V1 baseline can treat the creating coach as the only practical editor until multi-coach workspaces are implemented.
- Arabic inference must be explainable through reviewed terms and confidence scores; do not hide low-confidence mappings as exact matches.
Provider switchability
Provider switchability rule: changing providers should require a new ExerciseProviderPort adapter and provider mapping/backfill job, not changes to Program Builder, client workout logging, progress charts, or stored program exercise snapshots. The normalized value layer is ExerciseCatalogItem, ExerciseTaxonomyNode, ExerciseMediaRef, ExerciseSearchResultDto, and ExerciseSelectionSnapshot.
| Model | Owned By | Important Methods | Emits |
|---|---|---|---|
ExerciseProviderIntegration | ProgramsAndExercise | enable(), disable(), updateCapabilities(), assertCanSearch(), assertCanUseMedia() | ExerciseProviderConfigured, ExerciseProviderDisabled |
ExerciseCatalogItem | ProgramsAndExercise | refreshFromProvider(), createCustom(), attachMedia(), markDeprecated(), markUnavailable(), toSelectionSnapshot() | ExerciseCatalogItemRefreshed, CustomExerciseCreated, ExerciseCatalogItemDeprecated |
CoachExerciseMediaAsset | ProgramsAndExercise | requestUpload(), markProcessed(), attachToExercise(), replace() | CustomExerciseMediaUploaded, CustomExerciseMediaProcessed |
ExerciseExternalVideoRef | ProgramsAndExercise | validate(), markPlayable(), markFailed(), scheduleRecheck() | ExternalExerciseVideoValidationRequested, ExternalExerciseVideoValidationCompleted |
ExerciseTaxonomyNode | ProgramsAndExercise | refreshLabel(), linkToProvider(), retire() | ExerciseTaxonomyChanged |
ExerciseTranslationTerm | ProgramsAndExercise | approve(), reject(), match(), recordMiss() | ExerciseTranslationTermNeedsReview |
ProgramDraft | ProgramsAndExercise | Consumes ExerciseSelectionSnapshot when adding exercises. Owns sets, reps, order, alternatives, and coach notes. | ProgramExerciseAdded |
Table Structure
Persist provider configuration, normalized catalog data, coach-owned custom exercises, media assets, external video validation, taxonomy, translation terms, search cache, sync history, and selection audit references.
| Table | Purpose | Important Columns | Constraints & Indexes |
|---|---|---|---|
exercise_provider_integrations | Selected exercise content provider and capability policy. V1 baseline uses a global YMove integration with workspace_id null; keep the nullable column for later workspace-specific providers. | id, workspace_id, provider_key, status, credentials_ref, capabilities jsonb, enabled_locales text[], license_policy jsonb, plan_policy jsonb, detail_cache_ttl_days, last_health_status, last_synced_at, created_at, updated_at, disabled_at | Unique active global integration where workspace_id is null; future unique active provider per workspace; index (provider_key, status); credentials stored by reference only, never inline. |
exercise_taxonomy_nodes | Normalized provider and CoachMe-reviewed taxonomy for muscle groups, equipment, movement patterns, exercise types, levels, and modalities. | id, provider_integration_id, provider_taxonomy_id, taxonomy_type, code, parent_id, labels jsonb, sort_order, status, provider_updated_at, last_synced_at | Unique (provider_integration_id, taxonomy_type, provider_taxonomy_id); index (taxonomy_type, status, sort_order); parent references stay inside this table. |
exercise_catalog_items | Selectable exercises from either the third-party provider catalog or coach-created custom library. | id, workspace_id, source_type, owner_coach_user_id, provider_integration_id, provider_exercise_id, status, visibility, names jsonb, description jsonb, instructions jsonb, exercise_type_ids uuid[], primary_muscle_ids uuid[], secondary_muscle_ids uuid[], equipment_ids uuid[], movement_pattern_ids uuid[], difficulty, has_video, thumbnail_asset_id, external_video_ref_id, media_refs jsonb, search_document, content_hash, provider_updated_at, last_synced_at, deleted_at | Partial unique (provider_integration_id, provider_exercise_id) where source_type = 'PROVIDER'; optional unique active custom title per (workspace_id, normalized name) if product requires it; GIN index on search_document; GIN indexes for taxonomy node arrays such as exercise types, muscles, equipment, and movement patterns; partial index on active selectable items only. Store provider media refs for backend session resolution, not durable client-playable provider URLs. |
exercise_thumbnail_sources | Approved image libraries or owned asset sources that can be mapped to provider catalog items. | id, source_key, license_policy jsonb, attribution_policy jsonb, status, imported_at | Unique source_key; inactive sources stop new mappings but do not break historical snapshots. |
exercise_thumbnail_assets | Local image asset records imported from an approved source or created by CoachMe. | id, thumbnail_source_id, source_item_id, source_path, local_asset_key, width, height, sha256, license, source_url | Index (thumbnail_source_id, source_item_id); dedupe by sha256; retain provenance. |
exercise_thumbnail_mappings | Reviewed mapping from provider catalog items to local or licensed image assets. | id, exercise_catalog_item_id, thumbnail_asset_id, status, source, reviewed_by_user_id, reviewed_at, created_at | Only APPROVED mappings are used at runtime; unique active approved mapping per catalog item; index review status for admin queues. |
exercise_custom_media_assets | Coach-uploaded exercise videos and thumbnails stored in CoachMe-controlled object storage. | id, exercise_catalog_item_id, workspace_id, uploaded_by_user_id, asset_type, storage_key, original_filename, mime_type, size_bytes, duration_secs, width, height, sha256, processing_status, created_at, deleted_at | Index (exercise_catalog_item_id, asset_type, processing_status); only processed active assets can render; uploaded files are scanned and type-checked before activation. |
exercise_external_video_refs | External video URL references for coach-created exercises, including validation and renderability checks. | id, exercise_catalog_item_id, submitted_url, normalized_url, final_url, provider_kind, render_strategy, validation_status, http_status, content_type, embed_url, metadata jsonb, failure_reason, last_checked_at, created_by_user_id, created_at | Unique active ref per custom exercise; first-class embed handling for YOUTUBE, GOOGLE_DRIVE, and VIMEO, with direct video files handled by DIRECT_VIDEO_URL; validate only https/http; block localhost, private IP ranges, metadata services, and unsafe redirects; mark failed when the URL cannot be rendered by supported players. |
exercise_media_sessions | Short-lived playback sessions created by the backend media-session endpoint for provider, uploaded, or validated external exercise media. | id, exercise_catalog_item_id, workspace_id, actor_user_id, session_token_hash, media_source_type, provider_exercise_id, provider_session_ref, usage_policy_snapshot jsonb, expires_at, last_heartbeat_at, ended_at, created_at | Index (actor_user_id, created_at desc); index (provider_exercise_id, created_at desc); token values are hashed at rest; expired sessions cannot be reused. |
exercise_media_access_events | Append-only media access ledger for plan enforcement, session analytics, and usage reconciliation. | id, exercise_media_session_id, exercise_catalog_item_id, provider_exercise_id, actor_user_id, surface, accessed_at, video_duration_secs, counted_as_new_unique, provider_warning | Index (provider_exercise_id, accessed_at desc); use provider-specific policy before creating media sessions or calling media-enabled endpoints. |
exercise_search_cache | Short-lived cache for normalized local search result pages. | id, provider_integration_id, query_hash, locale, filters jsonb, source_filter, result_item_ids uuid[], source, expires_at, created_at | Unique (provider_integration_id, query_hash, locale, source_filter); index (expires_at); purge after expiry. |
exercise_translation_terms | Arabic/English synonyms, display labels, and inference terms for fallback search. | id, workspace_id, locale, source_term, normalized_term, target_term, term_type, taxonomy_node_id, confidence, review_status, miss_count, created_by_user_id, reviewed_by_user_id, created_at, reviewed_at | Unique reviewed term by (workspace_id, locale, normalized_term, term_type); index (review_status, miss_count desc). |
exercise_provider_sync_runs | Append-only sync, backfill, health-check, and provider import audit. | id, provider_integration_id, sync_type, status, started_at, finished_at, items_seen, items_created, items_updated, items_deprecated, error_code, error_summary, metadata jsonb | Index (provider_integration_id, started_at desc); append-only with retention or archival. |
program_workout_exercises | Program Builder owned row that consumes Exercise Library snapshots after selection. | exercise_catalog_item_id, exercise_source_type, nullable provider_key, nullable provider_exercise_id, exercise_snapshot jsonb, plus program-owned order, sets, reps, rest, effort, notes, and alternatives. | Do not rely on provider or mutable custom exercise data at render time. Store the snapshot needed to render assigned programs, logs, and historical reports. |
Implementation note
Use plain UUID references across bounded contexts. Exercise Library can reference users for admin review and Program Builder can reference catalog item ids, but provider catalog refresh must not mutate program drafts or assigned program aggregates directly.
Lifecycle Policy
Provider data changes frequently, but program history must stay stable. Current catalog state and immutable selected snapshots solve different problems.
Catalog Lifecycle
ACTIVE: searchable and selectable for new program drafts.DEPRECATED: hidden from new search and selection; existing program snapshots and workout history still render.UNAVAILABLE: hidden from new search and selection; existing snapshots still render.HIDDEN: excluded from coach search because of licensing, moderation, duplicate mapping, or product policy.- Use soft delete only for local cache cleanup after retention windows; do not delete records needed to explain existing program snapshots.
Provider & Sync Lifecycle
- Provider integrations can be active, paused, disabled, or failed-health-check.
- Credentials are rotated outside the row and referenced through
credentials_ref. - YMove starts as the single global provider. Re-check YMove subscription terms, usage limits, regional availability, and media URL policy before production launch.
- Provider detail cache TTL is provider-specific and must not exceed 14 days for selection; providers can require a shorter verification window.
- Sync runs are append-only for operational debugging and vendor support.
- Search cache expires quickly and can be purged without product data loss.
- Taxonomy changes create new labels or retired nodes rather than rewriting selected exercise snapshots.
Custom Exercise Lifecycle
DRAFT: coach can edit metadata and attach media before the exercise appears in search.PENDING_MEDIA: uploaded media, thumbnail processing, or external video URL validation is still running.ACTIVE: searchable, selectable, and labeled as a coach-owned exercise.ARCHIVED: hidden from new search and selection, but existing program snapshots still render.- Custom exercises default to workspace-shared visibility. V1 baseline does not need multi-coach management UI, but the data model should not require rewriting when workspace teams are added.
- Replacing an uploaded video, thumbnail, or URL reference updates the catalog item for future selections only; existing program snapshots keep their captured media refs.
Translation Lifecycle
- Machine or heuristic mappings start as
PENDING_REVIEWunless supplied by a trusted provider source. - Reviewed Arabic terms can be used for display labels and search inference.
- Rejected terms remain in history to avoid repeated bad suggestions.
- Miss counts help admins prioritize common unmapped queries.
Snapshot Stability
- Program exercise snapshots are immutable after insertion except for program-owned coach notes and exercise prescription fields.
- If a provider exercise is removed or a custom exercise is archived later, existing programs, workout logs, PR history, and progress graphs keep using their stored snapshot.
- Snapshot payloads should include source attribution, local thumbnail references, uploaded-media references, external URL render metadata, and runtime media eligibility only when the license and ownership policy permit display.
- Reports can join to current catalog data for enrichment, but rendering must work without that join.
API Contracts
Coach-facing APIs serve taxonomy, search, details, selection snapshots, and backend media sessions. Admin APIs manage provider health, thumbnail review, and translation review.
| Endpoint | Purpose | Request | Response |
|---|---|---|---|
GET /api/v1/workspaces/{workspaceId}/exercise-library/capabilities | Return provider and custom-exercise feature support for UI behavior. | Coach or admin workspace session. | ExerciseLibraryCapabilitiesDto with provider key, locales, Arabic support flags, media policy, custom exercise limits, allowed upload MIME types, max file sizes, and degraded-state flags. |
GET /api/v1/workspaces/{workspaceId}/exercise-library/taxonomy | List muscle groups, equipment, movement patterns, levels, modalities, and exercise type taxonomy nodes. | Query: type, locale, includeDeprecated. For type=exercise_type, return taxonomy nodes rather than an enum. | Ordered taxonomy nodes with labels, provider references where applicable, status, and parent ids. |
GET /api/v1/workspaces/{workspaceId}/exercise-library/search | Search local normalized provider catalog and coach custom exercises from Program Builder. | Query: q, locale, source=all|my-library|provider, exerciseTypeId, muscleGroupId, equipmentId, movementPatternId, cursor, limit. | Cursor page of ExerciseSearchResultDto with source type, source label, editable flag, labels, exercise type taxonomy chips, approved provider thumbnail, custom thumbnail, or placeholder, and selectable flag. |
GET /api/v1/workspaces/{workspaceId}/exercise-library/exercises/{exerciseId} | Read normalized exercise details before selection. | Query: locale, optional refresh=true. | Exercise detail with safe provider metadata, taxonomy, media refs, instructions where licensed, stale-cache age, and whether selection requires provider verification before snapshot creation. |
POST /api/v1/workspaces/{workspaceId}/exercise-library/exercises/{exerciseId}/media-session | Create a backend playback session from the selected provider when plan guard allows it, or resolve a coach-owned uploaded/external media reference through the same session contract. | Surface, actor context, idempotency key. | Short-lived CoachMe media-session DTO or typed limit response such as MEDIA_LIMIT_REACHED. Clients should play through the CoachMe session/proxy endpoint so viewing can be tracked, limited, and reconciled with provider policy; durable provider-hosted signed URLs are not returned directly. |
POST /api/v1/workspaces/{workspaceId}/exercise-library/exercises/{exerciseId}/selection-snapshot | Create a program-safe snapshot for insertion into a draft. | Program draft context, target locale, idempotency key. | ExerciseSelectionSnapshotDto and selectable-state validation result. |
POST /api/v1/workspaces/{workspaceId}/exercise-library/custom-exercises | Create a coach-owned custom exercise. | Name, exerciseTypeIds, instructions, cues, taxonomy ids, difficulty, optional visibility override, optional initial media mode, idempotency key. | Custom exercise detail with sourceType=COACH_CUSTOM, default visibility=WORKSPACE, exercise type taxonomy nodes, draft or pending-media status, and next required media action. |
PATCH /api/v1/workspaces/{workspaceId}/exercise-library/custom-exercises/{exerciseId} | Edit a coach-owned custom exercise. | Editable metadata, taxonomy ids, visibility, status transition, expected version. | Updated custom exercise detail; existing program snapshots are not rewritten. |
POST /api/v1/workspaces/{workspaceId}/exercise-library/custom-exercises/{exerciseId}/media-upload-intents | Create a signed upload intent for a custom video or thumbnail. | assetType=VIDEO|THUMBNAIL, filename, MIME type, byte size, checksum. | Signed upload URL, object key, constraints, and asset id. Activation waits for scan and processing callbacks. |
POST /api/v1/workspaces/{workspaceId}/exercise-library/custom-exercises/{exerciseId}/external-video-ref | Attach and validate an external video URL reference. | Submitted URL and optional provider hint. | Validation result with PENDING, PLAYABLE, or FAILED, render strategy, embed URL where supported, and failure reason if not renderable. |
POST /api/v1/workspaces/{workspaceId}/exercise-library/search-misses | Record no-result or low-confidence search terms for translation/content review. | Query text, locale, filters, surface, optional selected fallback. | Accepted audit id. |
GET /api/v1/admin/exercise-providers/{providerIntegrationId}/sync-runs | Admin/provider support view of sync health. | Admin actor, cursor, status filter. | Sync run page with counts, errors, durations, and metadata summaries. |
POST /api/v1/admin/exercise-providers/{providerIntegrationId}/sync | Trigger bootstrap, incremental sync, or health check. | Sync type, optional provider cursor, idempotency key. | Queued job id and current provider health. |
GET /api/v1/admin/exercise-thumbnail-mappings/review-queue | Review pending, low-confidence, or missing thumbnail mappings. | Admin actor, status filter, confidence range, provider exercise filter, cursor. | Queue items with provider exercise metadata, candidate thumbnail assets, source/license details, confidence score, usage priority, and unresolved-source reason. |
PATCH /api/v1/admin/exercise-thumbnail-mappings/{mappingId} | Approve, reject, replace, or mark thumbnail mapping as unresolved. | Status, optional replacement asset id, rejection reason, source-gap reason, review note. | Reviewed thumbnail mapping and cache invalidation status. |
GET /api/v1/admin/exercise-translation-terms/review-queue | Review Arabic search misses and synonym suggestions. | Admin actor, locale, term type, minimum miss count, review status, cursor. | Prioritized terms with source query, suggested English or taxonomy target, confidence, miss count, example surfaces, and current reviewer state. |
PATCH /api/v1/admin/exercise-translation-terms/{termId} | Approve, reject, or edit Arabic fallback terms. | Target term, term type, confidence, review status. | Reviewed translation term. |
Admin Review Workflows
Exercise Library needs small operational queues for content quality gaps that cannot be solved safely by automation alone.
Thumbnail Mapping Review
- Automated thumbnail matching creates candidate mappings with confidence, source provenance, license metadata, and an unresolved-source reason when no acceptable asset exists.
- Admin queue is sorted by missing thumbnail status, provider exercise popularity or recent selection attempts, low confidence, and source/license risk.
- Reviewer sees the provider exercise name, muscles, equipment, movement metadata, candidate thumbnail, source URL, license terms, and any existing placeholder.
- Reviewer can approve the candidate, reject it, replace it with another approved asset, upload a CoachMe-owned thumbnail, or mark the exercise as placeholder-only until an approved source is available.
- Only approved mappings render at runtime. Rejected and unresolved mappings remain auditable so the same bad candidate is not repeatedly suggested.
Arabic Translation Review
- Search misses and low-confidence Arabic matches are aggregated by normalized term, locale, filters, miss count, and selected fallback behavior.
- Admin queue prioritizes high miss count terms, terms that block provider exercise discovery, and suggestions that map to taxonomy nodes such as muscle, equipment, movement pattern, or exercise type.
- Reviewer can approve a synonym, edit the English target term, attach the term to a taxonomy node, merge duplicates, reject noisy terms, or leave a term pending for content review.
- Approved terms are available to the query normalizer after cache invalidation. Rejected terms stay in history with the reason to avoid repeated low-quality suggestions.
- Arabic display labels should be reviewed separately from search synonyms when the term changes customer-facing copy.
Key Code Snippets
Implementation sketches for the source-aware catalog in a TypeScript, NestJS, Drizzle, and OpenAPI modular monolith.
Drizzle schema shape
apps/api/src/programs-exercise/db/exercise-library.schema.ts
ts
export const exerciseProviderStatus = pgEnum("exercise_provider_status", [
"ACTIVE",
"PAUSED",
"DISABLED",
"FAILED_HEALTH_CHECK",
]);
export const exerciseCatalogItemStatus = pgEnum("exercise_catalog_item_status", [
"ACTIVE",
"DEPRECATED",
"UNAVAILABLE",
"HIDDEN",
]);
export const exerciseCatalogSourceType = pgEnum("exercise_catalog_source_type", [
"PROVIDER",
"COACH_CUSTOM",
]);
export const exerciseTaxonomyType = pgEnum("exercise_taxonomy_type", [
"muscle_group",
"equipment",
"movement_pattern",
"exercise_type",
"difficulty",
"modality",
]);
export const customMediaAssetType = pgEnum("exercise_custom_media_asset_type", [
"VIDEO",
"THUMBNAIL",
]);
export const externalVideoValidationStatus = pgEnum("external_video_validation_status", [
"PENDING",
"PLAYABLE",
"FAILED",
]);
export type ProviderKey = string;
export const exerciseProviderIntegrations = pgTable("exercise_provider_integrations", {
id: uuid("id").primaryKey().defaultRandom(),
workspaceId: uuid("workspace_id"),
providerKey: text("provider_key").notNull(),
status: exerciseProviderStatus("status").notNull().default("ACTIVE"),
credentialsRef: text("credentials_ref").notNull(),
capabilities: jsonb("capabilities").$type<ExerciseProviderCapabilities>().notNull(),
enabledLocales: text("enabled_locales").array().notNull().default(["en"]),
licensePolicy: jsonb("license_policy").$type<ExerciseLicensePolicy>().notNull(),
planPolicy: jsonb("plan_policy").$type<ExerciseProviderPlanPolicy>().notNull(),
detailCacheTtlDays: integer("detail_cache_ttl_days").notNull().default(14),
lastHealthStatus: text("last_health_status").notNull().default("UNKNOWN"),
lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }),
disabledAt: timestamp("disabled_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
activeGlobalProviderUq: uniqueIndex("exercise_provider_active_global_uq")
.on(table.status)
.where(sql`${table.status} = 'ACTIVE' and ${table.workspaceId} is null`),
activeWorkspaceProviderUq: uniqueIndex("exercise_provider_active_workspace_uq")
.on(table.workspaceId, table.status)
.where(sql`${table.status} = 'ACTIVE' and ${table.workspaceId} is not null`),
providerStatusIdx: index("exercise_provider_status_idx")
.on(table.providerKey, table.status),
}));
export const exerciseTaxonomyNodes = pgTable("exercise_taxonomy_nodes", {
id: uuid("id").primaryKey().defaultRandom(),
providerIntegrationId: uuid("provider_integration_id"),
providerTaxonomyId: text("provider_taxonomy_id"),
taxonomyType: exerciseTaxonomyType("taxonomy_type").notNull(),
code: text("code").notNull(),
parentId: uuid("parent_id"),
labels: jsonb("labels").$type<LocalizedText>().notNull(),
sortOrder: integer("sort_order").notNull().default(0),
status: text("status").notNull().default("ACTIVE"),
providerUpdatedAt: timestamp("provider_updated_at", { withTimezone: true }),
lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }).notNull(),
}, (table) => ({
providerTaxonomyUq: uniqueIndex("exercise_taxonomy_provider_taxonomy_uq")
.on(table.providerIntegrationId, table.taxonomyType, table.providerTaxonomyId)
.where(sql`${table.providerTaxonomyId} is not null`),
taxonomyCodeIdx: index("exercise_taxonomy_code_idx")
.on(table.providerIntegrationId, table.taxonomyType, table.code),
taxonomyListIdx: index("exercise_taxonomy_list_idx")
.on(table.providerIntegrationId, table.taxonomyType, table.status, table.sortOrder),
}));
export const exerciseCatalogItems = pgTable("exercise_catalog_items", {
id: uuid("id").primaryKey().defaultRandom(),
workspaceId: uuid("workspace_id"),
sourceType: exerciseCatalogSourceType("source_type").notNull(),
ownerCoachUserId: uuid("owner_coach_user_id"),
providerIntegrationId: uuid("provider_integration_id"),
providerExerciseId: text("provider_exercise_id"),
status: exerciseCatalogItemStatus("status").notNull().default("ACTIVE"),
visibility: text("visibility").notNull().default("WORKSPACE"),
names: jsonb("names").$type<LocalizedText>().notNull(),
description: jsonb("description").$type<LocalizedText | null>(),
instructions: jsonb("instructions").$type<LocalizedText | null>(),
exerciseTypeIds: uuid("exercise_type_ids").array().notNull().default([]),
primaryMuscleIds: uuid("primary_muscle_ids").array().notNull().default([]),
secondaryMuscleIds: uuid("secondary_muscle_ids").array().notNull().default([]),
equipmentIds: uuid("equipment_ids").array().notNull().default([]),
movementPatternIds: uuid("movement_pattern_ids").array().notNull().default([]),
difficulty: text("difficulty").$type<ExerciseDifficulty>(),
hasVideo: boolean("has_video").notNull().default(false),
thumbnailAssetId: uuid("thumbnail_asset_id"),
externalVideoRefId: uuid("external_video_ref_id"),
mediaRefs: jsonb("media_refs").$type<ExerciseMediaRef[]>().notNull().default([]),
searchDocument: text("search_document").notNull(),
contentHash: text("content_hash").notNull(),
providerUpdatedAt: timestamp("provider_updated_at", { withTimezone: true }),
lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }).notNull(),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
}, (table) => ({
providerExerciseUq: uniqueIndex("exercise_catalog_provider_exercise_uq")
.on(table.providerIntegrationId, table.providerExerciseId)
.where(sql`${table.sourceType} = 'PROVIDER'`),
searchIdx: index("exercise_catalog_search_idx")
.using("gin", sql`to_tsvector('simple', ${table.searchDocument})`),
exerciseTypeIdx: index("exercise_catalog_exercise_type_idx")
.using("gin", table.exerciseTypeIds),
primaryMuscleIdx: index("exercise_catalog_primary_muscle_idx")
.using("gin", table.primaryMuscleIds),
secondaryMuscleIdx: index("exercise_catalog_secondary_muscle_idx")
.using("gin", table.secondaryMuscleIds),
equipmentIdx: index("exercise_catalog_equipment_idx")
.using("gin", table.equipmentIds),
movementPatternIdx: index("exercise_catalog_movement_pattern_idx")
.using("gin", table.movementPatternIds),
sourceIdx: index("exercise_catalog_source_idx")
.on(table.workspaceId, table.sourceType, table.status),
selectableIdx: index("exercise_catalog_selectable_idx")
.on(table.workspaceId, table.sourceType, table.status)
.where(sql`${table.status} = 'ACTIVE' and ${table.deletedAt} is null`),
}));
export const exerciseCustomMediaAssets = pgTable("exercise_custom_media_assets", {
id: uuid("id").primaryKey().defaultRandom(),
exerciseCatalogItemId: uuid("exercise_catalog_item_id").notNull(),
workspaceId: uuid("workspace_id").notNull(),
uploadedByUserId: uuid("uploaded_by_user_id").notNull(),
assetType: customMediaAssetType("asset_type").notNull(),
storageKey: text("storage_key").notNull(),
mimeType: text("mime_type").notNull(),
sizeBytes: integer("size_bytes").notNull(),
durationSecs: integer("duration_secs"),
sha256: text("sha256").notNull(),
processingStatus: text("processing_status").notNull().default("PENDING"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
});
export const exerciseExternalVideoRefs = pgTable("exercise_external_video_refs", {
id: uuid("id").primaryKey().defaultRandom(),
exerciseCatalogItemId: uuid("exercise_catalog_item_id").notNull(),
submittedUrl: text("submitted_url").notNull(),
normalizedUrl: text("normalized_url").notNull(),
finalUrl: text("final_url"),
providerKind: text("provider_kind").notNull().default("GENERIC_URL"),
renderStrategy: text("render_strategy"),
validationStatus: externalVideoValidationStatus("validation_status").notNull().default("PENDING"),
httpStatus: integer("http_status"),
contentType: text("content_type"),
embedUrl: text("embed_url"),
metadata: jsonb("metadata").$type<ExternalVideoMetadata | null>(),
failureReason: text("failure_reason"),
lastCheckedAt: timestamp("last_checked_at", { withTimezone: true }),
});
export const exerciseMediaSessions = pgTable("exercise_media_sessions", {
id: uuid("id").primaryKey().defaultRandom(),
exerciseCatalogItemId: uuid("exercise_catalog_item_id").notNull(),
workspaceId: uuid("workspace_id").notNull(),
actorUserId: uuid("actor_user_id").notNull(),
sessionTokenHash: text("session_token_hash").notNull(),
mediaSourceType: text("media_source_type").notNull(),
providerExerciseId: text("provider_exercise_id"),
providerSessionRef: text("provider_session_ref"),
usagePolicySnapshot: jsonb("usage_policy_snapshot").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),
endedAt: timestamp("ended_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});Provider adapter port
apps/api/src/programs-exercise/ports/exercise-provider.port.ts
ts
export interface ExerciseProviderPort {
readonly providerKey: ProviderKey;
getCapabilities(): Promise<ExerciseProviderCapabilities>;
listTaxonomy(input: ProviderTaxonomyRequest): Promise<ProviderTaxonomyPage>;
searchExercises(input: ProviderExerciseSearchRequest): Promise<ProviderExerciseSearchPage>;
getExerciseDetail(input: ProviderExerciseDetailRequest): Promise<ProviderExerciseDetail>;
}
export type ProviderExerciseSearchRequest = {
query: string;
locale: string;
filters: ExerciseSearchFilters;
cursor?: string;
limit: number;
allowArabicFallback: boolean;
};Provider registry
apps/api/src/programs-exercise/providers/exercise-provider.registry.ts
ts
@Injectable()
export class ExerciseProviderRegistry {
private readonly adapters = new Map<ProviderKey, ExerciseProviderPort>();
constructor(
adapters: ExerciseProviderPort[],
) {
adapters.forEach((adapter) => {
this.adapters.set(adapter.providerKey, adapter);
});
}
get(providerKey: ProviderKey): ExerciseProviderPort {
const adapter = this.adapters.get(providerKey);
if (!adapter) throw new UnsupportedExerciseProviderError(providerKey);
return adapter;
}
}Search query service
apps/api/src/programs-exercise/queries/search-exercise-library.ts
ts
@Injectable()
export class SearchExerciseLibraryQuery {
constructor(
private readonly integrations: ExerciseProviderIntegrationRepository,
private readonly catalog: ExerciseCatalogRepository,
private readonly normalizer: ExerciseQueryNormalizer,
private readonly policy: ExerciseLibraryAccessPolicy,
) {}
async execute(query: ExerciseSearchRequest, actor: Actor): Promise<CursorPage<ExerciseSearchResultDto>> {
this.policy.assertCanSearch(actor, query.workspaceId);
const integration = await this.integrations.getActiveForWorkspace(query.workspaceId);
integration.assertCanSearch(query.locale);
const normalized = await this.normalizer.normalize({
query: query.q,
locale: query.locale,
filters: query.filters,
capabilities: integration.capabilities,
});
const page = await this.catalog.searchLocal({
providerIntegrationId: integration.id,
workspaceId: query.workspaceId,
normalized,
sourceFilter: query.source ?? "ALL",
ownerCoachUserId: actor.userId,
cursor: query.cursor,
limit: query.limit,
includeApprovedThumbnail: true,
includeCustomThumbnail: true,
});
return ExerciseSearchResultDto.pageFromCatalog(page.items, page.nextCursor, normalized);
}
}Arabic query normalizer
apps/api/src/programs-exercise/services/exercise-query-normalizer.ts
ts
export class ExerciseQueryNormalizer {
constructor(private readonly terms: ExerciseTranslationTermRepository) {}
async normalize(input: NormalizeExerciseQueryInput): Promise<NormalizedExerciseQuery> {
if (input.capabilities.supportsArabicSearch || input.locale !== "ar") {
return NormalizedExerciseQuery.direct(input.query, input.locale, input.filters);
}
const tokens = tokenizeArabicExerciseQuery(input.query);
const matches = await this.terms.matchReviewedTerms({
locale: "ar",
tokens,
filters: input.filters,
});
const providerQuery = matches.map((match) => match.targetTerm).join(" ").trim();
if (!providerQuery) {
return NormalizedExerciseQuery.lowConfidence(input.query, "en", input.filters);
}
return NormalizedExerciseQuery.fallback({
originalQuery: input.query,
providerQuery,
providerLocale: "en",
filters: input.filters.withInferredTaxonomy(matches),
confidence: averageConfidence(matches),
explanation: matches.map((match) => match.explain()),
});
}
}Selection snapshot use case
apps/api/src/programs-exercise/use-cases/create-exercise-selection-snapshot.ts
ts
@Injectable()
export class CreateExerciseSelectionSnapshotUseCase {
constructor(
private readonly catalog: ExerciseCatalogRepository,
private readonly providerDetails: ExerciseProviderDetailService,
private readonly customMedia: CustomExerciseMediaResolver,
private readonly policy: ExerciseLibraryAccessPolicy,
private readonly tx: TransactionRunner,
) {}
async execute(command: CreateExerciseSelectionSnapshotCommand, actor: Actor): Promise<ExerciseSelectionSnapshotDto> {
this.policy.assertCanSelectExercise(actor, command.workspaceId, command.programDraftId);
return this.tx.run(async () => {
const item = await this.catalog.getSelectable(command.exerciseCatalogItemId);
const resolved = item.sourceType === "PROVIDER"
? await this.providerDetails.verifyFreshEnoughForSelection(item, command.locale)
: await this.customMedia.resolvePlayableRefs(item, actor);
const snapshot = resolved.toSelectionSnapshot({
locale: command.locale,
includeMedia: this.policy.canUseExerciseMedia(actor, resolved),
});
return ExerciseSelectionSnapshotDto.fromValue(snapshot);
});
}
}External video URL validator
apps/api/src/programs-exercise/services/external-video-url-validator.ts
ts
@Injectable()
export class ExternalVideoUrlValidator {
constructor(
private readonly networkGuard: PublicUrlNetworkGuard,
private readonly probe: RemoteMediaProbe,
private readonly embeds: KnownVideoEmbedRegistry,
) {}
async validate(input: ValidateExternalVideoUrlInput): Promise<ExternalVideoValidationResult> {
const parsed = parseHttpUrl(input.url);
await this.networkGuard.assertPublicHttpUrl(parsed);
const knownEmbed = this.embeds.match(parsed);
if (knownEmbed) {
const metadata = await knownEmbed.fetchOEmbed(parsed);
return ExternalVideoValidationResult.playable({
normalizedUrl: knownEmbed.normalizedWatchUrl,
providerKind: knownEmbed.providerKind,
renderStrategy: "EMBED",
embedUrl: knownEmbed.embedUrl,
metadata,
});
}
const response = await this.probe.headOrRangeGet(parsed, { maxRedirects: 3 });
await this.networkGuard.assertPublicHttpUrl(response.finalUrl);
if (!response.contentType.startsWith("video/")) {
return ExternalVideoValidationResult.failed("UNSUPPORTED_CONTENT_TYPE", response);
}
return ExternalVideoValidationResult.playable({
normalizedUrl: parsed.href,
finalUrl: response.finalUrl.href,
providerKind: "DIRECT_VIDEO_URL",
renderStrategy: "HTML5_VIDEO",
contentType: response.contentType,
});
}
}Events & Background Jobs
Provider sync, custom media processing, and URL validation are asynchronous where possible. Program insertion stays synchronous enough for the builder to feel immediate.
Events
ExerciseProviderConfigured: schedule bootstrap sync and provider health check.ExerciseProviderHealthChanged: update admin dashboard and degraded-state flags.ExerciseTaxonomyChanged: invalidate taxonomy API and client caches.ExerciseCatalogItemRefreshed: invalidate exercise detail and search result caches.ExerciseCatalogItemDeprecated: remove from new search and selection while preserving existing snapshot rendering.CustomExerciseCreated/CustomExerciseUpdated/CustomExerciseArchived: invalidate source-filtered search and recently used exercise lists.CustomExerciseMediaUploaded: schedule scan, metadata extraction, thumbnail processing, or transcode checks.ExternalExerciseVideoValidationCompleted: update custom exercise media readiness and notify the creating coach when validation fails.ExerciseMediaSessionCreated: record playback start, plan guard result, provider media reference, and initial usage policy snapshot.ExerciseTranslationTermNeedsReview: notify admin/content queue when repeated search misses occur.ExerciseSelectionSnapshotCreated: optional audit event for program-draft insertion traceability.
Jobs
- Provider bootstrap sync imports full provider taxonomy and initial catalog metadata according to the provider's caching and media policy.
- Incremental sync refreshes changed exercises, media eligibility flags, deprecated provider exercises, and retired taxonomy nodes.
- Provider health check records latency, quota, auth, and stale-cache status.
- Thumbnail source import stores approved image assets and source provenance.
- Thumbnail matching job creates candidate mappings, requests GPT ranking only for ambiguous rows, and queues low-confidence or unresolved-source results for review.
- Custom media processing scans coach uploads, extracts duration and dimensions, validates MIME type, creates optimized thumbnails where supported, and marks assets active or failed.
- External video validation checks submitted URLs, safe redirects, content type, first-class embed providers, oEmbed metadata, and player render strategy before the URL can be used in an active custom exercise.
- Provider usage reconciliation compares local media sessions and access events with provider usage snapshots when the provider exposes them.
- Search cache cleanup purges expired query result rows.
- Translation miss aggregation groups repeated Arabic no-result queries for review.
- Catalog backfill rebuilds
search_documentand content hashes after normalization changes. - Provider switch backfill maps a newly selected provider into the same normalized taxonomy and catalog structures before enabling it for coach search.
Provider synchronization
Provider webhook support is optional. If a provider has no webhooks, use scheduled incremental sync and on-demand detail refresh for selected exercises.
Permissions & Access Rules
Most catalog data is not client-private, but provider licensing and workspace entitlements still require enforcement.
Coach
- Coach must have active workspace membership and program-builder permission to search and select exercises.
- Coach can read taxonomy, search results, and details for enabled provider content and permitted coach-created content in the workspace.
- Coach can create, edit, archive, and attach media for workspace-shared custom exercises when their role allows it; V1 baseline can limit editing to the creating coach until multi-coach workspace roles ship.
- Coach can filter search to all exercises, My Library, or Provider Library; edit controls appear only on custom exercises the coach can manage.
- Coach cannot edit provider catalog metadata in Public V1.
- Coach-specific notes, cues, substitutions, and exercise prescription fields are saved on program draft rows.
- Coach can select only items allowed by provider license, catalog status, and program-builder policy.
Admin, Client & System
- System admin can configure the global YMove integration, run sync jobs, inspect health, review thumbnail mappings, and review translation terms.
- Client mobile reads exercise snapshots through assigned workouts, not the full Exercise Library catalog.
- Client media access must respect provider media license, custom exercise ownership, backend media-session/proxy rules, and external URL validation status captured in the assigned snapshot.
- System workers can sync provider data through internal service accounts with scoped credentials.
- System workers validate external video URLs from a locked-down network path that blocks private IP ranges, localhost, cloud metadata addresses, unsupported schemes, and unsafe redirects.
- Provider credentials are never returned through API responses or stored in outbox payloads.
Web, Coach Mobile, Client Mobile
All surfaces use the same contracts, but only coach surfaces use the catalog search directly in Public V1.
Coach Web
- Dense program-builder search with muscle-group sidebar, exercise type taxonomy filter, equipment filters, source segmented control, focused overlay while typing, keyboard navigation, and quick insertion.
- Provides clear filters for All, My Library, and Provider Library.
- Shows source attribution on every result card: coach-owned exercises use a My Exercise label and provider exercises use the third-party provider label.
- Can open exercise detail side panel before adding to a day or block, and can open create/edit custom exercise flows from the library surface.
- Custom exercise creation supports uploaded video, uploaded thumbnail, or external video URL reference with first-class handling for YouTube, Google Drive, and Vimeo plus validation status visible before the exercise becomes selectable.
- Displays degraded provider state, missing-thumbnail placeholders, media-limit messages, and stale-cache indicators without losing the current program draft.
Coach Mobile
- Uses bottom-sheet or full-screen focused search from the mobile program builder.
- Fetches taxonomy and source-filtered search results from the same APIs as web.
- Uses compact segmented controls or tabs for All, My Library, and Provider Library, with source badges and exercise type taxonomy chips on result rows.
- Can create a custom exercise with mobile media picker upload or a pasted external video URL, then shows pending, playable, or failed validation state.
- Caches recent taxonomy and selected exercise details for draft continuity.
- Offline mode can show recently used exercises for reference, but new selections should sync through the backend before publishing.
Client Mobile
- Does not browse the full catalog in Public V1.
- Opens a contextual exercise detail sheet from any named assigned exercise — the current set in a guided session, a logged workout in history; there is no client Library tab.
- The sheet always renders snapshot instructions, program-row coach cues, equipment, and local thumbnails. Video playback requires an online media session and degrades to the thumbnail plus a typed unavailable or limit-reached state when offline, unlicensed, unvalidated, or over the plan limit.
- Receives assigned workout exercises as program-owned snapshots with source attribution, local thumbnail references, custom uploaded media refs or validated external video refs, provider media-session eligibility, and instructions.
- Starts playback through the CoachMe media-session/proxy endpoint so provider and custom video usage can be tracked and limited.
- Uses snapshots for offline workout logging, set completion, substitutions approved by the coach, and historical workout review.
- Sync sends performed exercise references from assigned workout structure, not arbitrary provider catalog ids.
Test Scenarios & Confirmed Decisions
Validate provider isolation, coach-owned custom exercise behavior, deterministic selection snapshots, URL validation, Arabic fallback behavior, media-session limits, and degradation paths before Program Builder depends on the library.
Tests
- Domain: active provider with supported locale can search; disabled provider rejects search and selection.
- Domain: configured provider key resolves through the registry without leaking provider-specific DTOs to application services.
- Domain: only active catalog items are selectable for new program drafts; deprecated provider exercises render only through existing snapshots.
- Domain: custom and provider exercises must link exercise type values through
ExerciseTaxonomyNoderecords withtaxonomy_type = exercise_type. - Domain: coach-created custom exercise can move from draft to active only after required metadata is present and attached media or external URL validation is successful.
- Domain: selection snapshot remains stable after catalog item name, taxonomy, or media references change.
- Domain: selection snapshot includes
sourceTypeand preserves provider attribution or coach-custom owner attribution. - Adapter contract: each provider adapter maps provider taxonomy, exercise ids, media refs, pagination, and error codes into canonical DTOs.
- Adapter contract: provider switch changes adapter/backfill behavior without changing Program Builder insertion contracts.
- API: coach without program-builder permission cannot search or create selection snapshots.
- API: coach cannot edit or attach media to a custom exercise outside the workspace-shared custom exercise policy.
- API: search source filters return correct result sets for all exercises, My Library, and Provider Library.
- API: invalid or retired exercise type taxonomy node ids are rejected for custom exercise creation and search filters.
- API: admin-only provider sync, thumbnail-review, and translation-review endpoints reject coach and client actors.
- API: media-session endpoint creates short-lived sessions, records access events, enforces provider limits, and does not return durable provider-hosted signed URLs directly to clients.
- Integration: provider outage returns degraded local-cache results and records provider health change.
- Integration: cached provider exercise details older than the provider TTL, capped at 14 days, trigger verification or block selection if verification fails.
- Integration: repeated provider catalog sync upserts do not duplicate catalog items.
- Integration: uploaded custom video and thumbnail assets are scanned, MIME checked, processed, and activated before appearing in selectable search results.
- Integration: external video URL validator supports first-class YouTube, Google Drive, and Vimeo embeds; rejects invalid syntax, unsupported schemes, private-network redirects, unsupported content types, and URLs that cannot be embedded or played.
- Integration: Arabic query without provider Arabic support maps reviewed terms to English and records low-confidence misses.
- Admin workflow: approved thumbnail mappings render at runtime; rejected or unresolved-source mappings keep using placeholders and remain auditable.
- Admin workflow: approved Arabic synonym and taxonomy terms are available to search after cache invalidation; rejected suggestions are not repeatedly requeued.
- Web E2E: coach opens Program Builder, filters by muscle group and source, searches with local thumbnails, opens focused overlay, selects provider and custom exercises, and sees them inserted into the active block with distinct labels.
- Coach mobile E2E: coach creates a custom exercise from a bottom sheet, validates a pasted video URL, filters to My Library, selects the exercise, returns to the draft, and can configure sets and alternatives.
- Client mobile E2E: assigned workout renders provider and custom exercise snapshots and media references offline without querying provider catalog or coach library APIs.
Confirmed Decisions
- V1 baseline provider is YMove. Before production launch, re-check YMove terms, pricing, usage limits, media rules, and regional availability against the chosen subscription.
- Provider configuration is global for V1 baseline. Keep
workspace_idnullable so workspace-specific providers can be added later without reshaping the table. - Provider detail cache TTL is provider-specific and capped at 14 days for selection. Older details require provider verification before a snapshot can be created.
- Deprecated provider exercises are not selectable for new programs. They render only from existing program snapshots and history.
- Playback goes through a CoachMe backend media-session/proxy endpoint so video access can be tracked, limited, and reconciled with provider usage rules.
- Thumbnail review uses an admin queue for pending, low-confidence, rejected, and unresolved-source mappings. Runtime uses only approved mappings or placeholders.
- Arabic translation review uses an admin queue for search misses and synonym suggestions prioritized by miss count, confidence, and taxonomy impact.
- Custom exercises default to workspace-shared visibility. Multi-coach workspace controls can be added later and are not required for V1 baseline.
- First-class external video handling starts with YouTube, Google Drive, and Vimeo, plus generic direct video URLs where safe and playable.
- Client exercise info is a contextual detail sheet opened wherever an assigned exercise is named, not a Library tab; full-catalog browsing stays coach-side in Public V1. Decided July 2026 with the guided Today-first client direction.