/** Mirrors the backend contract in ../src/core/types.ts — keep the two in sync by hand. */ /** * One spec row as the store prints it ("Augstums" / "92 mm"). Flag-style * attributes without a value ("Ar atgriezējvārstu") go in value with label "". */ export interface OfferAttribute { label: string; value: string; } export interface Offer { store: string; brand: string | null; partNumber: string; title: string; priceEur: number; availability: string | null; deliveryNote: string | null; url: string; imageUrl: string | null; attributes: OfferAttribute[]; } export interface StoreInfo { id: string; displayName: string; homepage: string; /** "auto" fetches on every search; "onDemand" (walled) renders idle until the * user asks and may need a challenge solved. Defaults to "auto". See WALLED-STORES.md. */ fetchMode?: 'auto' | 'onDemand'; } export interface SearchResponse { store: string; displayName: string; query: string; ok: boolean; offers: Offer[]; cached: boolean; durationMs: number; error?: string; /** Walled store hit a Cloudflare challenge; open `solveUrl` (noVNC) then retry. */ needsSolve?: boolean; solveUrl?: string; } export type StoreStatus = | 'idle' // onDemand/walled store, not fetched yet — waiting for the user to tap "Check" | 'searching' | 'needs_solve' // walled store hit a Cloudflare challenge; open solveUrl (noVNC) then retry | 'done' | 'empty' | 'error'; export interface StoreResult { status: StoreStatus; offers: Offer[]; /** noVNC stream to open when status is 'needs_solve'. */ solveUrl?: string; } /** * One product variant: the same physical part (brand + part number) offered by one * or more stores. Different part numbers of the same brand stay separate on purpose — * e.g. a two-piece alu-hub brake disc vs. the plain steel one for the same axle. */ export interface VariantGroup { key: string; brand: string | null; partNumber: string; title: string; offers: Offer[]; // sorted cheapest first minPriceEur: number; anyInStock: boolean; /** True when this is the part number the user actually typed (vs. an analogue). */ exactMatch: boolean; /** First image any of the group's offers carries (offers are price-sorted, so cheapest store's photo wins). */ imageUrl: string | null; /** Specs from whichever offer describes the part in most detail — stores list the same physical part. */ attributes: OfferAttribute[]; } /** "Nav pieejams" / "Nav noliktavā" style texts mean out of stock; anything else present means in stock. */ export function isInStock(o: Offer): boolean { return o.availability !== null && !/^nav\b/i.test(o.availability.trim()); }