"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.js — getRepoAveragesInRange()
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
"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_snapshotsholds(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:
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_countover 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_appsis every instance minuscontainrrr/watchtower, so the movers must use the same population. Do not applyCATEGORY_EXCLUDE— that is a category rule, andtotal_appscounts 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_snapshotsa0means the collector failed that day, which is why the KPI code treats zero as missing.repo_snapshotsis 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_snapshotsrows 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
Newrather than a percentage, matching howcomputeChange()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 asget_daily_revenue_in_range: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 SQLAVG() ... GROUP BY image_name, no row cap to work around.Scope
supabase/migrations/— new RPCsqliteAdapter.js+supabaseAdapter.js—getRepoAveragesInRange()src/lib/kpi/metrics.js— build the movers list, apply grouping and the watchtower exclusionsrc/lib/kpi/discord.js— render the field, 3 rows, no emoji, aligned in a code block|delta|, new/disappeared apps, and a failed-collection day excluded for all images