feat: link tackle analytics to canonical gear
This commit is contained in:
@@ -56,6 +56,20 @@ def tackle_combinations(
|
||||
result = []
|
||||
for (role, value), reports in groups.items():
|
||||
unique_reports = {report.id: report for report in reports}
|
||||
item_ids = {
|
||||
component.tackle_item_id
|
||||
for report in unique_reports.values()
|
||||
for component in report.tackle_components
|
||||
if component.role == role and component.raw_value.strip() == value and component.tackle_item_id is not None
|
||||
}
|
||||
rig_ids = {
|
||||
component.rig_id
|
||||
for report in unique_reports.values()
|
||||
for component in report.tackle_components
|
||||
if component.role == role and component.raw_value.strip() == value and component.rig_id is not None
|
||||
}
|
||||
canonical_item_id = next(iter(item_ids)) if len(item_ids) == 1 and not rig_ids else None
|
||||
canonical_rig_id = next(iter(rig_ids)) if len(rig_ids) == 1 and not item_ids else None
|
||||
players = {report.player_name.strip().casefold() for report in unique_reports.values() if report.player_name and report.player_name.strip()}
|
||||
catches = len(unique_reports)
|
||||
unique_players = len(players)
|
||||
@@ -66,7 +80,8 @@ def tackle_combinations(
|
||||
) / catches * 100)
|
||||
enough = catches >= min_samples and unique_players >= min_players
|
||||
result.append(TackleCombinationOut(
|
||||
role=role, value=value, catches=catches, unique_players=unique_players,
|
||||
role=role, value=value, tackle_item_id=canonical_item_id, rig_id=canonical_rig_id,
|
||||
catches=catches, unique_players=unique_players,
|
||||
last_seen_at=last_seen, freshness_score=freshness_score,
|
||||
status="recommendation" if enough else "insufficient_data",
|
||||
explanation=(
|
||||
|
||||
@@ -115,6 +115,8 @@ class PaginatedActivityOut(BaseModel):
|
||||
class TackleCombinationOut(BaseModel):
|
||||
role: str
|
||||
value: str
|
||||
tackle_item_id: UUID | None = None
|
||||
rig_id: UUID | None = None
|
||||
catches: int
|
||||
unique_players: int
|
||||
last_seen_at: datetime
|
||||
|
||||
@@ -22,7 +22,7 @@ export type Catch = { id: string; fish: string; weight_g: number; bait: string |
|
||||
export type DictionaryItem = { id: string; slug: string; name_ru: string; unlock_level?: number | null; fish_species_count?: number | null; source_system?: string | null; source_external_id?: string | null; source_url?: string | null; description?: string | null; source_aliases?: string[] | null; source_fish_species?: string[] | null; source_image_urls?: string[] | null; source_point_urls?: string[] | null; source_checked_at?: string | null };
|
||||
export type TackleItem = { id: string; name: string; category: string; subcategory: string | null; brand: string | null; family: string | null; unlock_level: number | null; source_system: string | null; source_external_id: string | null; source_url: string | null; source_checked_at: string | null; missing_fields: string[] };
|
||||
export type PaginatedTackleItems = { items: TackleItem[]; total: number; limit: number; offset: number };
|
||||
export type TackleCombination = { role: string; value: string; catches: number; unique_players: number; last_seen_at: string; freshness_score: number; status: "recommendation" | "insufficient_data"; explanation: string };
|
||||
export type TackleCombination = { role: string; value: string; tackle_item_id: string | null; rig_id: string | null; catches: number; unique_players: number; last_seen_at: string; freshness_score: number; status: "recommendation" | "insufficient_data"; explanation: string };
|
||||
export type OfficialRecord = { id: string; fish: string; weight_g: number; waterbody: string; bait: string | null; player_name: string | null; record_date: string | null; category: string | null; region: string | null; source_url: string | null; source_system: string };
|
||||
export type PaginatedOfficialRecord = {
|
||||
items: OfficialRecord[];
|
||||
|
||||
@@ -36,5 +36,5 @@ const roleLabels: Record<string, string> = { lure: "Приманка", bait: "Н
|
||||
{(waterbody || fish || method || hours !== 72) && <a data-action="quiet" href="/tackle/analytics">Сбросить</a>}
|
||||
</form>
|
||||
<p class="privacy content-grid">Порог рекомендации: минимум 3 наблюдения от 2 независимых игроков. Это не рейтинг снасти и не гарантия улова.</p>
|
||||
{unavailable ? <StatePanel tone="unavailable" title="Аналитика временно недоступна" description="Не показываем непроверенные сочетания. Попробуйте позже." /> : combinations.length ? <section class="signal-grid content-grid" aria-label="Сочетания снастей" data-analytics-results>{combinations.map(item => <article class="signal-card" data-analytics-status={item.status}><div class="signal-card__top"><span class="source-chip"><span>{roleLabels[item.role] ?? item.role}</span></span><span class="quality-chip">{item.status === "recommendation" ? "Рекомендация" : "Недостаточно данных"}</span></div><h2>{item.value}</h2><dl><div><dt>Наблюдения</dt><dd>{item.catches}</dd></div><div><dt>Игроки</dt><dd>{item.unique_players}</dd></div><div><dt>Свежесть выборки</dt><dd>{item.freshness_score}%</dd></div><div><dt>Последнее</dt><dd>{ago(item.last_seen_at)}</dd></div></dl><p>{item.explanation}</p></article>)}</section> : <div data-analytics-results><StatePanel title="Подтверждённых сочетаний пока нет" description="Сочетания появятся после новых одобренных наблюдений с указанием компонентов." /></div>}
|
||||
{unavailable ? <StatePanel tone="unavailable" title="Аналитика временно недоступна" description="Не показываем непроверенные сочетания. Попробуйте позже." /> : combinations.length ? <section class="signal-grid content-grid" aria-label="Сочетания снастей" data-analytics-results>{combinations.map(item => { const canonicalHref = item.rig_id ? `/tackle/rigs/${item.rig_id}` : item.tackle_item_id ? `/tackle/items/${item.tackle_item_id}` : null; return <article class="signal-card" data-analytics-status={item.status}><div class="signal-card__top"><span class="source-chip"><span>{roleLabels[item.role] ?? item.role}</span></span><span class="quality-chip">{item.status === "recommendation" ? "Рекомендация" : "Недостаточно данных"}</span></div><h2>{canonicalHref ? <a href={canonicalHref}>{item.value}</a> : item.value}</h2><dl><div><dt>Наблюдения</dt><dd>{item.catches}</dd></div><div><dt>Игроки</dt><dd>{item.unique_players}</dd></div><div><dt>Свежесть выборки</dt><dd>{item.freshness_score}%</dd></div><div><dt>Последнее</dt><dd>{ago(item.last_seen_at)}</dd></div></dl><p>{item.explanation}</p></article>})}</section> : <div data-analytics-results><StatePanel title="Подтверждённых сочетаний пока нет" description="Сочетания появятся после новых одобренных наблюдений с указанием компонентов." /></div>}
|
||||
</Layout>
|
||||
|
||||
Reference in New Issue
Block a user