feat(ypp): the three eligibility bars, with dated thresholds (TASK 39·2)
YouTube moves the goalposts, so the app stops modelling them as its own truth and shows progress against a threshold table that carries the date it was read. - `Models/YppProgress.cs` — pure: three metrics, per-criterion met, and `MeetsPublishedRule`, which encodes the real shape of the rule (subscribers AND either view route). The drawer says "x of 3 criteria met — YouTube reviews eligibility" and spells out the either/or, so "2 of 3" can't be misread. - `Services/YouTubeAnalyticsService.cs` — the two numbers only Analytics publishes: long-form watch hours (trailing 12 months, `creatorContentType==video`) and Shorts-feed views (90 days, `==shorts`). No rows means NO DATA, not zero. - **Thresholds are data with a publication date** (`YppThresholds.AsOfUtc`, shown in the drawer) and a test fails once that reading is more than 180 days old. The 2027-02-01 doubling (4,000h → 8,000h, 10M → 20M Shorts views) resolves from the date, not from an edit. - **No Analytics scope → no invented numbers:** the drawer explains the missing consent instead of drawing two empty bars as zero. Money stays behind 3·20's "Show monetary metrics" toggle. - Fan-funding tier (500 subs) demoted to a single muted line, since the three bars are the ad-revenue path. Tests: `YppProgressTests` — either/or eligibility, subscribers alone insufficient, Shorts as a qualifying route, bars clamp at 100%, the doubling date, the staleness guard, Analytics parsing (no rows = no data), and compact value formatting. FAST gate: 567 passed, 0 failed, 0 warnings.
This commit is contained in:
+56
-46
@@ -768,56 +768,66 @@
|
||||
<StackPanel Visibility="{Binding Ypp.IsSignedOut, Converter={StaticResource InverseBoolToVis}}">
|
||||
<TextBlock Text="{Binding Ypp.DisplayName}" Foreground="{StaticResource Brush.OnSurface}" FontSize="13"
|
||||
FontWeight="SemiBold" Margin="0,0,0,10"/>
|
||||
<TextBlock Text="SUBSCRIBERS — TIER 1 · FAN FUNDING" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||||
<TextBlock Text="{Binding Ypp.Tier1SubsText}" Foreground="{StaticResource Brush.OnSurface}" FontSize="13"/>
|
||||
<Border Height="8" Background="{StaticResource Brush.SurfaceContainerHigh}" CornerRadius="4" Margin="0,4,0,10">
|
||||
<!-- TASK 39·2 — the three metrics that count, with the threshold
|
||||
that applies TODAY (the long-form bar doubles 2027-02-01; that is
|
||||
data, not a code change). YouTube decides eligibility; these are
|
||||
estimates and the drawer says so. No red/green scoring. -->
|
||||
<TextBlock Text="SUBSCRIBERS" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||||
<DockPanel Margin="0,0,0,2">
|
||||
<TextBlock DockPanel.Dock="Right" Text="{Binding Ypp.SubscribersText}"
|
||||
Foreground="{StaticResource Brush.OnSurface}" FontSize="12"
|
||||
FontFamily="{StaticResource FontFamily.Mono}"/>
|
||||
<TextBlock Text="Ad revenue tier" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11"/>
|
||||
</DockPanel>
|
||||
<Border Height="8" Background="{StaticResource Brush.SurfaceContainerHigh}" CornerRadius="4" Margin="0,0,0,10">
|
||||
<Border Height="8" Background="{StaticResource Brush.Tertiary}" CornerRadius="4" HorizontalAlignment="Left"
|
||||
Width="{Binding Ypp.Tier1SubsBarWidth}"/>
|
||||
Width="{Binding Ypp.SubscribersBarWidth}"/>
|
||||
</Border>
|
||||
<TextBlock Text="SUBSCRIBERS — TIER 2 · AD REVENUE" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||||
<TextBlock Text="{Binding Ypp.Tier2SubsText}" Foreground="{StaticResource Brush.OnSurface}" FontSize="13"/>
|
||||
<Border Height="8" Background="{StaticResource Brush.SurfaceContainerHigh}" CornerRadius="4" Margin="0,4,0,10">
|
||||
<Border Height="8" Background="{StaticResource Brush.Tertiary}" CornerRadius="4" HorizontalAlignment="Left"
|
||||
Width="{Binding Ypp.Tier2SubsBarWidth}"/>
|
||||
</Border>
|
||||
<TextBlock Text="PUBLIC UPLOADS (90 DAYS)" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||||
<TextBlock Text="{Binding Ypp.UploadsText}" Foreground="{StaticResource Brush.OnSurface}" FontSize="13"/>
|
||||
<Border Height="8" Background="{StaticResource Brush.SurfaceContainerHigh}" CornerRadius="4" Margin="0,4,0,6">
|
||||
<Border Height="8" Background="{StaticResource Brush.Tertiary}" CornerRadius="4" HorizontalAlignment="Left"
|
||||
Width="{Binding Ypp.UploadsBarWidth}"/>
|
||||
</Border>
|
||||
<TextBlock Text="LONG-FORM WATCH HOURS + SHORTS VIEWS" Style="{StaticResource YtLabel}" Margin="0,10,0,2"/>
|
||||
<TextBlock Text="These arrive with the extended analytics update — subscriber + upload + standing progress is live now."
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="12" TextWrapping="Wrap"/>
|
||||
|
||||
<!-- TASK 3·20 — THIS SESSION. Counts are neutral and always
|
||||
shown when there were events; money appears only if the
|
||||
creator turned it on AND there is something to show, so a
|
||||
creator who never monetized never sees a zero. Derived from
|
||||
YouTube on demand; nothing is stored. -->
|
||||
<StackPanel Visibility="{Binding Ypp.HasSessionRewards, Converter={StaticResource BoolToVis}}">
|
||||
<TextBlock Text="THIS SESSION" Style="{StaticResource YtLabel}" Margin="0,14,0,2"/>
|
||||
<TextBlock Text="{Binding Ypp.SessionCountsText}"
|
||||
Foreground="{StaticResource Brush.OnSurface}" FontSize="13"/>
|
||||
<ItemsControl ItemsSource="{Binding Ypp.SessionMoneyLines}"
|
||||
Visibility="{Binding Ypp.HasSessionMoney, Converter={StaticResource BoolToVis}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" Foreground="{StaticResource Brush.OnSurfaceVariant}"
|
||||
FontFamily="{StaticResource FontFamily.Mono}" FontSize="12" Margin="0,2,0,0"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Estimates from YouTube — removed or refunded events are excluded and totals can change."
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11"
|
||||
TextWrapping="Wrap" Margin="0,4,0,0"
|
||||
Visibility="{Binding Ypp.HasSessionMoney, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="LONG-FORM WATCH HOURS · 12 MONTHS" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||||
<DockPanel Margin="0,0,0,2">
|
||||
<TextBlock DockPanel.Dock="Right" Text="{Binding Ypp.WatchHoursText}"
|
||||
Foreground="{StaticResource Brush.OnSurface}" FontSize="12"
|
||||
FontFamily="{StaticResource FontFamily.Mono}"/>
|
||||
<TextBlock Text="Public videos" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11"/>
|
||||
</DockPanel>
|
||||
<Border Height="8" Background="{StaticResource Brush.SurfaceContainerHigh}" CornerRadius="4" Margin="0,0,0,4">
|
||||
<Border Height="8" Background="{StaticResource Brush.Tertiary}" CornerRadius="4" HorizontalAlignment="Left"
|
||||
Width="{Binding Ypp.WatchHoursBarWidth}"/>
|
||||
</Border>
|
||||
<TextBlock Text="Bar rises to 8,000 h on 1 Feb 2027."
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="0,0,0,10"/>
|
||||
|
||||
<CheckBox Content="Show monetary metrics" Style="{StaticResource YtCheckBox}"
|
||||
FontSize="12" Margin="0,10,0,0"
|
||||
IsChecked="{Binding Ypp.ShowMonetaryMetrics, Mode=TwoWay}"
|
||||
ToolTip="Off by default. Counts still appear; amounts stay hidden until you ask for them."/>
|
||||
<TextBlock Text="SHORTS VIEWS · 90 DAYS" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||||
<DockPanel Margin="0,0,0,2">
|
||||
<TextBlock DockPanel.Dock="Right" Text="{Binding Ypp.ShortsViewsText}"
|
||||
Foreground="{StaticResource Brush.OnSurface}" FontSize="12"
|
||||
FontFamily="{StaticResource FontFamily.Mono}"/>
|
||||
<TextBlock Text="Shorts feed" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11"/>
|
||||
</DockPanel>
|
||||
<Border Height="8" Background="{StaticResource Brush.SurfaceContainerHigh}" CornerRadius="4" Margin="0,0,0,8">
|
||||
<Border Height="8" Background="{StaticResource Brush.Tertiary}" CornerRadius="4" HorizontalAlignment="Left"
|
||||
Width="{Binding Ypp.ShortsViewsBarWidth}"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="{Binding Ypp.CriteriaMetText}"
|
||||
Foreground="{StaticResource Brush.OnSurface}" FontSize="12" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" Margin="0,0,0,2"/>
|
||||
<TextBlock Text="Either view route qualifies — subscribers plus watch hours, or subscribers plus Shorts views."
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11"
|
||||
TextWrapping="Wrap" Margin="0,0,0,4"/>
|
||||
<TextBlock Text="{Binding Ypp.ThresholdsAsOfText}"
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11"
|
||||
TextWrapping="Wrap" Margin="0,0,0,8"/>
|
||||
|
||||
<TextBlock Text="{Binding Ypp.WatchMetricsMissingText}"
|
||||
Foreground="{StaticResource Brush.Attention}" FontSize="11" TextWrapping="Wrap"
|
||||
Margin="0,0,0,8"
|
||||
Visibility="{Binding Ypp.HasWatchMetrics, Converter={StaticResource InverseBoolToVis}}"/>
|
||||
|
||||
<TextBlock Text="FAN FUNDING TIER · 500 SUBSCRIBERS" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||||
<TextBlock Text="{Binding Ypp.Tier1SubsText}" Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11"
|
||||
FontFamily="{StaticResource FontFamily.Mono}" Margin="0,0,0,10"/>
|
||||
|
||||
<TextBlock Text="COMPLIANCE CHECKLIST" Style="{StaticResource YtLabel}" Margin="0,14,0,4"/>
|
||||
<TextBlock Text="CHANNEL STANDING" Style="{StaticResource YtLabel}" Margin="0,0,0,2"/>
|
||||
|
||||
Reference in New Issue
Block a user