Skip to content

Admin Feed Imports

This doc describes how an administrator browses external-provider sports data and imports it into Foundry through the Feed Imports admin UI.

Note on history. An earlier design proposed a dedicated provider abstraction (IFeedProvider / FoxSportsFeedProvider), per-provider browse queries (BrowseFeedCompetitionsQuery etc.), a ToggleCompetitionAutoImportCommand, and a FeedSyncTimerService. That design was never merged and none of those types exist in the codebase. The feature shipped differently: a generic, provider-agnostic admin wizard backed by the existing import infrastructure. This doc reflects what actually exists.

Overview

Feed Imports is an admin-only wizard that lets you drill into harvested provider data (sport -> provider -> entity) and trigger imports per entity. It has two halves:

  • Browse -- read-only listings served by LBS.Api endpoints under /api/admin/feed-imports/*, sourced from ClickHouse harvest tables and joined against the entity-mapping crosswalk to show whether each row is already linked/imported.
  • Import -- an POST /api/import job using one of the generic per-entity importers (GenericCompetitions, GenericSeasons, GenericSportingEvents, GenericTeams, GenericParticipants, GenericVenues). These run on the standard import job queue; see Import Infrastructure Usage for the queue, executor, retry and result-tracking details.

UI structure

The wizard lives under src/Apps/foundry-web/src/routes/admin/feed-imports/ and is a nested route hierarchy:

/admin/feed-imports                                              pick a sport
/admin/feed-imports/[sport]                                      pick a provider
/admin/feed-imports/[sport]/[provider]                           pick an entity area
/admin/feed-imports/[sport]/[provider]/competitions             competitions list (per-row import)
.../competitions/[competitionId]/seasons                        seasons (per-row import)
.../competitions/[competitionId]/seasons/[seasonId]/fixtures    fixtures (bulk import)
/admin/feed-imports/[sport]/[provider]/teams                    teams (flat, searchable)
/admin/feed-imports/[sport]/[provider]/participants             participants (flat, searchable)
/admin/feed-imports/[sport]/[provider]/venues                   venues grouped by country
.../venues/[country]                                            venues for a country

Sport and provider option lists come from $lib/constants/theme (allSports, allProviders). Each entity page shows two status badges per row:

  • ExistenceBadge -- a tri-state existenceStatus (NotLinked, LinkedNotImported, Imported) telling you what is still missing before/after import.
  • ProviderBadges -- which other providers are already mapped to the same canonical entity.

Entry points elsewhere in the app:

  • Sidebar (src/Apps/foundry-web/src/lib/components/layout/sidebar.svelte): Importers (/admin/importers) and Feed Imports (/admin/feed-imports).
  • Competitions list page (src/Apps/foundry-web/src/routes/competitions/+page.svelte): an admin-only Import from Provider button that navigates to /admin/feed-imports.

Browse flow

Each entity page fetches its listing through the shared helper src/Apps/foundry-web/src/lib/components/feed-imports/api.ts (fetchJson + buildQuery), which attaches the bearer token and calls the matching LBS.Api endpoint. For example the competitions page requests:

GET /api/admin/feed-imports/competitions?provider={provider}&sport={sport}

The endpoints live under src/Apps/LBS.Api/Features/Admin/FeedImports/:

Route Endpoint
GET /api/admin/feed-imports/competitions BrowseCompetitionsEndpoint
GET /api/admin/feed-imports/seasons BrowseSeasonsEndpoint
GET /api/admin/feed-imports/sporting-events BrowseSportingEventsEndpoint
GET /api/admin/feed-imports/teams BrowseTeamsEndpoint
GET /api/admin/feed-imports/participants BrowseParticipantsEndpoint
GET /api/admin/feed-imports/venues BrowseVenuesEndpoint
GET /api/admin/feed-imports/venues/countries GetVenueCountriesEndpoint

All browse endpoints are admin-gated (this.Policies(AuthorizationPolicyDefinition.RequireAdmin)) and read from the ClickHouse harvest tables (foundry.competitions, etc.) via IClickHouseRepository. Each row is LEFT JOINed to foundry.entity_mapping_crosswalk to resolve a canonical id, then IEntityMappingService and the Marten document store are consulted to compute mappedProviders and existenceStatus. Responses are paged (FeedImportResponse<T> with Items + TotalCount).

Import flow

When an admin clicks an Import button, the page calls runImport(...) from feed-imports/api.ts, which POSTs to /api/import with a generic importer type and a GenericImportData payload, then redirects to the importer's job-history page:

// runImport('GenericCompetitions', provider, sport, { competitionProviderId })
POST /api/import
{
  "importerType": "GenericCompetitions",
  "priority": "High",
  "importData": {
    "provider": { "name": "<provider>" },
    "sport": { "name": "<sport>" },
    "competitionProviderId": "<providerId>"   // optional FK narrowing
  }
}

The contract is GenericImportData (src/Integration/LBS.Fantasy.Integration/Importers/Generic/GenericImportData.cs): Provider is always required; Sport is required for sport-keyed entities (venues do not use it); and the optional CompetitionProviderId, SeasonProviderId, and Country filters narrow the import (fixture-tree drill-down, or the venue browser).

The generic importers live in src/Integration/LBS.Fantasy.Integration/Importers/Generic/. Each carries an [Importer("...")] attribute (whose name matches the [DataContract(Name = "GenericImportData")]) and is gated with [RequiresModule(ModuleDefinition.SportCore)] because it depends on the native ClickHouse repository and entity-mapping service:

Importer type Class
GenericCompetitions GenericCompetitionImporter
GenericSeasons GenericSeasonImporter
GenericSportingEvents GenericSportingEventImporter
GenericTeams GenericTeamImporter
GenericParticipants GenericParticipantImporter
GenericVenues GenericVenueImporter

Each importer re-queries the relevant ClickHouse harvest table for the requested provider/sport (optionally narrowed by the FK filters), then emits domain commands for the import infrastructure to execute. The full lifecycle -- queueing, the ImportProcessingBackgroundService, the ImportExecutor, transactional batching by aggregate, retries, and ImportExecutionResult tracking -- is the standard pipeline documented in Import Infrastructure Usage.

POST /api/import itself is handled by ImportEndpoint (src/Domain/LBS.Domain.Infrastructure/Import/ImportEndpoint.cs): it validates the importer type via IImporterTypeRegistry, and either runs High-priority jobs synchronously when the queue is empty or queues them for the background worker.

Job history and available importers

  • Available importers: /admin/importers (src/Apps/foundry-web/src/routes/admin/importers/+page.svelte) lists every registered importer by querying AvailableImporters through the SDK (AvailableImportersQuery -> AvailableImportersQueryHandler, admin-only). This includes the generic importers above alongside all other importers in the system.
  • Job history: /admin/importers/[type] (src/Apps/foundry-web/src/routes/admin/importers/[type]/+page.svelte) shows the jobs and results for a given importer type using the AllImportJobs query. runImport redirects here after submitting, so the admin lands on the live job status for what they just kicked off.

Adding a new provider or sport

Because browse and import are both provider-agnostic, onboarding a new provider/sport for an existing entity type is usually a matter of:

  1. Harvesting the provider's data into the relevant foundry.* ClickHouse table (handled by the data harvesters, not this UI).
  2. Adding the provider/sport to the option lists in $lib/constants/theme.

No new browse endpoint or importer is needed unless you are adding a brand-new entity type.