Skip to content

Backend

  • Status: Five-card evaluation deployed on OVH and exercised on the XT1058
  • Technology: .NET 10, C# 14, ASP.NET Core Minimal API

The backend keeps modern, changing integrations away from Android 5.1. It authenticates to configured device identities, collects and selects provider content, renders bounded image cards, and publishes immutable manifests and assets. The Android app remains a native client. This describes the checked-in implementation; the current deployment receipt records the exact running image. The earlier receipt records the MLB Stats edge rejection that prompted this evaluation.

What exists

The solution contains an API project and an xUnit test project. A single hosted refresh loop fetches due sources in the background, with no provider work in device requests. It permits at most two concurrent source fetches and recomposes only profiles that use a changed source. The API process is stateless across restarts and exposes:

Route Current role
GET / Service identity
GET /health/live Process liveness
GET /health/ready Readiness for deployment checks
GET /api/v1/dashboard Authenticated, profile-specific schema-1 manifest
GET /api/v1/assets/{sha256}.png Authenticated immutable rendered card image

Both device routes authenticate a bearer token against its configured SHA-256 hash and resolve the assigned profile. The manifest and assets support conditional ETag requests. Assets are addressed by their PNG SHA-256 and receive immutable cache headers. The process keeps the current snapshot and up to two prior snapshots per profile in memory so assets from a recently replaced manifest remain available. IFeedSnapshotStore is the snapshot store boundary. Integration tests exercise the full application with WebApplicationFactory.

The checked-in evaluation configuration assigns five sources to bedroom: Bogotá weather (Open-Meteo), two Yankees cards (API-Sports Baseball and BALLDONTLIE MLB), and NYT and Guardian RSS headlines. The earlier MLB Stats source remains defined but disabled. Sources and profile assignments are configuration-driven, but this checkout does not include an administration or enrollment interface.

Baseball provider evaluation

The deployed image registers two keyed baseball adapters and selects both for separate, attributed evaluation cards. The previous OVH release selected MlbStats; the current release disables it. The alternative adapter uses API-Sports team ID 25, requests the local yesterday/today/tomorrow dates without a season/team filter, and selects the configured team from shared date snapshots. In the observed free plan, the current season was denied when a season parameter was present, whereas date-only current games worked. Adjacent dates may be denied as the rolling free window crosses UTC midnight; a denied optional date is skipped until the next UTC day. Today remains required. The adapter keeps a per-process ceiling of 90 upstream requests per UTC day, below the provider's 100/day free quota. Its cached day snapshots are not durable across API restarts. Normal refreshes are no faster than 30 minutes, and live-game refreshes no faster than 15 minutes. Multiple time zones consume separate date snapshots and share that same request ceiling. The source's missing key or a provider entitlement error leaves the previous valid dashboard snapshot in place through the existing failure policy. Live cards expire 20 minutes after their last successful confirmation, so the 15-minute permitted refresh does not leave a recurring 10-minute gap; if refreshes fail, the stale live card still disappears.

The adapter is selected by a server source definition, not by the Android contract. It reuses the game selector and sports-card renderer, so adding phones does not multiply upstream calls. Its 90/day ceiling and date caches reset when the API process restarts; they are not a durable account-wide quota ledger. The exact image was rehearsed and both cards were accepted over public HTTPS; longer-term freshness and provider reliability remain evaluation work.

BALLDONTLIE uses team ID 19 and one team-filtered request for yesterday through seven days ahead, with up to 100 games. A non-null pagination cursor is treated as an incomplete response rather than publishing a misleading next game. Its documented free access is five requests per minute; the normal refresh floor is 15 minutes and live games refresh every two minutes. The adapter maps status_state, runs, and inning into the same GameContent as the other baseball adapters. A scheduled game's placeholder zeroes are not published as a 0–0 score. Both keys stay server-side. See Deployment for activation and rollback.

Implemented pipeline

flowchart LR
    Providers[Open-Meteo / API-Sports / BALLDONTLIE / RSS] --> Adapters[Provider adapters]
    Disabled[MLB Stats - disabled] -. not selected .-> Adapters
    Adapters --> Loop[RefreshCoordinator and hosted loop]
    Loop --> Composer[FeedComposer]
    Composer --> Renderer[SkiaSharp renderer]
    Renderer --> Snapshots[IFeedSnapshotStore]
    Snapshots --> Contract[Versioned dashboard manifest]
    Snapshots --> Media[Hash-addressed PNG assets]
    Contract --> Device[Android client]
    Media --> Device

Minimal API endpoints stay focused on HTTP transport and authorization. Provider adapters normalize external payloads; the refresh coordinator owns source state, ETags, due times, deadlines, and bounded concurrency; FeedComposer selects valid source content per profile and asks the renderer for cards. The snapshot store publishes a complete immutable manifest plus its referenced image bytes. The Android app downloads missing images, verifies size and hash, stages them, and activates a manifest only after all its assets are available.

The shared schema-1 fixtures under contracts/dashboard/v1/ are read by the backend and Android contract tests. Keep additions compatible with older clients and update both sides when the contract changes.

Card rendering and container

SkiaCardRenderer produces opaque 1280×720 PNGs. SkiaSharp and its Linux native assets are pinned together at version 4.153.1; the API project targets linux-musl-x64 and the final image is .NET 10 Alpine. IBM Plex Sans Regular and SemiBold are bundled with their SIL Open Font License text. tzdata is installed in the image so profile IANA zones such as America/Bogota resolve. The container sets DOTNET_gcServer=0 (workstation GC) and runs as the non-root app user. Published configuration, fonts and binaries are owned by that user so a private umask 077 preparation remains readable; the production read-only root filesystem still prevents modification. These are checked-in image settings, not production resource measurements.

Packaging and deployment boundary

The API has a production container definition intended for loopback port 8088 on the OVH host, reached through system Nginx; the container listens on 8080. The 1 October provider release is deployed; the new dual-provider evaluation is not yet part of that release. Rendered dashboard images are currently stored in process memory and served by the API, not from a filesystem /assets directory, filesystem cache, or Nginx static files. A process restart discards runtime snapshots; the API serves an empty cold-start manifest until source results produce a new profile snapshot. Durable photographs and music would need a separately backed-up store outside the container and release directories.

DNS, HTTPS, VPS state, and deployment status are documented in the operational runbook; they are distinct from the local implementation status above. Runtime device credentials are supplied through environment configuration and are not checked in. See OVH deployment and ADR 0002.