Skip to main content

Documentation Index

Fetch the complete documentation index at: https://285e39fd5e337e58f16290.sightscreen.app/llms.txt

Use this file to discover all available pages before exploring further.

Code generation pipeline

All API-facing models are auto-generated from backend Zod schemas via a two-stage pipeline. This ensures the iOS app and backend always agree on data shapes.

Two-stage codegen

StageToolInputOutput
1zod-to-json-schemaZod schema definitions in TypeScriptJSON Schema files
2quicktypeJSON Schema filesSwift Codable structs
Never hand-edit any .generated.swift file. Changes will be overwritten on the next generation run. If the model needs to change, update the Zod schema in the backend and regenerate.

Running the generator

# From the backend project root
npm run generate:schemas
This reads the Zod schemas, converts them to JSON Schema, then runs quicktype to produce Swift files with matching Codable structs.

Shared between targets

All generated files are included in both the main app target and the widget target (SightscreenWidget). This guarantees that the widget’s Live Activity rendering uses the exact same types as the main app.

Generated model files

LiveActivityModels.generated.swift

The primary generated file for Live Activity payloads. Contains the full display state hierarchy.
TypePurpose
LiveActivityDisplayStateTop-level display state for all LA presentations
LiveActivityCompactDisplay data for compact leading and trailing views
LiveActivityExpandedDisplay data for expanded Dynamic Island and Lock Screen
LiveActivityAttributesStatic data set at creation (match ID, team info) — cannot change
ContentStateDynamic data updated via APNs pushes (scores, overs, events)
MatchPhaseEnum: pre, live, innings_break, completed, abandoned
BattingPerspectiveWhich team is batting — drives display orientation
MatchSentimentEnum for match mood: neutral, exciting, tense, dominant
LiveActivityAttributes is set once at creation time and cannot change. ContentState is updated with every push. Anything that changes during a match must be in ContentState.

FollowModels.generated.swift

Models for the follow/unfollow system.
TypePurpose
FollowResponseBackend response after follow/unfollow — confirms state
FollowStatusEnum: following, not_following
UserFollowsResponseResponse from GET /user/follows — list of followed match IDs

HubAPIModels.generated.swift

Models for the main hub/schedule API responses.
TypePurpose
MatchItemA single match with pre-formatted display fields
LeagueGroupA group of matches under a league header
ScheduleResponseTop-level schedule response — array of LeagueGroup
LiveMatchesResponseResponse for the live matches endpoint

Hand-written models

Some models are hand-written because they contain client-side logic or do not map to a backend schema.

Auth models

TypePurpose
AppUserThe authenticated user — includes isAdmin flag derived from JWT cognito:groups

Core models

TypePurpose
TeamTeam identity — name, abbreviation, logo URL
MatchFull match data from the API
MatchDisplayStatePre-formatted display state for a match (computed by backend)

Model conventions

All models (generated and hand-written) follow these conventions:
ConventionDetail
CodableAll models conform to Codable for JSON serialization
HashableAll models conform to Hashable for use in SwiftUI lists and diffing
SendableAll models conform to Sendable for safe use across Swift 6 concurrency boundaries
struct MatchItem: Codable, Hashable, Sendable {
    let matchId: String
    let league: String
    let homeTeam: Team
    let awayTeam: Team
    let displayState: MatchDisplayState
    let startTime: Date
}

JSON decoding strategy

The app configures a shared JSONDecoder with ISO 8601 date support that handles both fractional seconds and standard formats.
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
    let container = try decoder.singleValueContainer()
    let dateString = try container.decode(String.self)

    // Try ISO 8601 with fractional seconds first
    let formatterWithFraction = ISO8601DateFormatter()
    formatterWithFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    if let date = formatterWithFraction.date(from: dateString) {
        return date
    }

    // Fall back to standard ISO 8601
    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withInternetDateTime]
    if let date = formatter.date(from: dateString) {
        return date
    }

    throw DecodingError.dataCorruptedError(
        in: container,
        debugDescription: "Invalid date: \(dateString)"
    )
}
The dual-format decoder exists because some backend responses include fractional seconds (e.g., 2024-03-15T10:30:00.000Z) and others do not (e.g., 2024-03-15T10:30:00Z). Both must decode correctly.

API format notes

ConcernDetail
Key formatcamelCase (matches Swift Codable defaults)
Date formatISO 8601 with or without fractional seconds
CodingKeysUsed to map any snake_case API keys to camelCase Swift properties
Custom decodersComplex transformations live in init(from:) implementations
Most transformations are handled automatically by CodingKeys enums. Only use custom init(from:) implementations for genuinely complex mappings.