Skip to content

feat: Notable movers section in the KPI report #71

Description

@2ndtlmining

"Total Apps +217" says the network grew but not what grew. A short Notable section naming the three images that moved the count most would turn the Applications section from a number into a story, and the data to do it already exists.

Feasibility: yes

repo_snapshots holds (snapshot_date, image_name, instance_count) per image per day, back to 2024-06-07 — 257,429 rows across 2,040 distinct images. That is exactly the per-image time series this needs. No new collection required.

Proposed output

One extra Discord embed field, capped at three rows for space:

Notable - biggest movers in Total Apps
Image                      Change    Share of net
palworld-server-docker        +92         42% of +217
minecraft-server              +31         14% of +217
presearch/node                -18        -8% of +217

Design decisions

Aggregation must match the Applications section. Total Apps is the average of daily readings across the period, so a mover's value must be the average of its daily instance_count over the same period. Using an end-of-period reading instead would produce movers that do not reconcile with the total they claim to explain.

Group by canonical name. Reuse groupReposByCanonicalName() so Minecraft Java + Bedrock read as one row, matching the category cards. Otherwise one popular app can occupy all three slots as separate image variants.

Exclude watchtower, nothing else. total_apps is every instance minus containrrr/watchtower, so the movers must use the same population. Do not apply CATEGORY_EXCLUDE — that is a category rule, and total_apps counts uncategorised images too.

Rank by absolute change, so a large drop is as notable as a large rise. Take the top 3 by |delta|.

Two traps worth getting right

1. The shares do not sum to 100%, and must not look like they do.

The net change is the sum of ~2,040 individual movements where risers and fallers offset each other. So a single mover's share of the net can exceed 100% (if others moved the other way), and a faller's share is negative while the total rises. Both are correct and both look like bugs to a reader.

Either word it explicitly — +92 (42% of the net +217) — with a one-line note that movers are individual contributions to a net figure and do not sum to it, or drop the share column and show each app's own percentage change instead. The second is less confusing but answers a different question than the one asked.

2. Absence means zero here, unlike everywhere else in the report.

For daily_snapshots a 0 means the collector failed that day, which is why the KPI code treats zero as missing. repo_snapshots is the opposite: a missing row means that image genuinely had no instances that day. Applying the existing zero-as-missing rule would silently discard every app that appeared or disappeared — precisely the most notable movers.

The guard needed is different: check whether any repo_snapshots rows exist for a given date. If none do, the collection failed and that day must be excluded for every image. If rows exist but not for image X, X really was at zero.

Follows from that: an image with no presence in the comparison period is New rather than a percentage, matching how computeChange() already handles a zero baseline.

Implementation note

Two weekly periods span roughly 28,500 rows (2,040 images x 14 days), well over PostgREST's 1000-row cap, so this cannot be a plain select() — it needs server-side aggregation. Add an RPC in the same shape as get_daily_revenue_in_range:

get_repo_averages_in_range(p_start DATE, p_end DATE)
  -> image_name, avg_instances, days_present

Call it once per period, join the two results in JS, and compute deltas there. A matching SQLite implementation is required in sqliteAdapter.js — plain SQL AVG() ... GROUP BY image_name, no row cap to work around.

Scope

  • supabase/migrations/ — new RPC
  • sqliteAdapter.js + supabaseAdapter.jsgetRepoAveragesInRange()
  • src/lib/kpi/metrics.js — build the movers list, apply grouping and the watchtower exclusion
  • src/lib/kpi/discord.js — render the field, 3 rows, no emoji, aligned in a code block
  • README — how movers are chosen, and the caveat that shares do not sum to the net
  • Tests — grouping, ranking by |delta|, new/disappeared apps, and a failed-collection day excluded for all images

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestkpiKPI report feature

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions