@@ -16,7 +16,7 @@ import type {
1616 DataSource ,
1717 Sentiment ,
1818} from "../types" ;
19- import type { DailyAppRanking , DailySnapshot , RankingsHistory } from "../utils" ;
19+ import type { DailySnapshot , RankingsHistory } from "../utils" ;
2020import {
2121 formatDateKey ,
2222 loadRankingsHistory ,
@@ -289,73 +289,123 @@ export const getSentiment = (trends: AppTrends): Sentiment | undefined => {
289289 return "neutral" ;
290290} ;
291291
292- /** Format the position text for a single app (e.g., "#35 overall · #12 Finance"). */
293- export const formatPositionText = (
292+ /** Format the position text for a Finance-section item (rank only, no " Finance" label ). */
293+ export const formatFinancePositionText = (
294294 app : TrackedApp ,
295- ranking : DailyAppRanking ,
296- ) : string => {
297- const parts : string [ ] = [ app . name ] ;
298-
299- if ( ranking . overall === null && ranking . finance === null ) {
300- parts . push ( "unranked" ) ;
301- } else if ( ranking . overall !== null && ranking . finance !== null ) {
302- parts . push (
303- `${ formatRank ( ranking . overall ) } overall · ${ formatRank ( ranking . finance ) } Finance` ,
304- ) ;
305- } else if ( ranking . finance === null ) {
306- parts . push ( `${ formatRank ( ranking . overall ) } overall` ) ;
307- } else {
308- parts . push ( `${ formatRank ( ranking . finance ) } Finance` ) ;
309- }
295+ rank : number ,
296+ ) : string => `${ app . name } : ${ formatRank ( rank ) } ` ;
310297
311- return parts . join ( ": " ) ;
312- } ;
298+ /** Format the position text for a Total-section item (rank only, no "overall" label). */
299+ export const formatOverallPositionText = (
300+ app : TrackedApp ,
301+ rank : number ,
302+ ) : string => `${ app . name } : ${ formatRank ( rank ) } ` ;
313303
314- /** Build BriefingItems from current rankings + historical trends, sorted by best position. */
315- const buildBriefingItems = (
304+ /** Compute daily/weekly/monthly trends for Overall rank of a single app. */
305+ const computeOverallAppTrends = (
306+ history : RankingsHistory ,
307+ currentRank : number | null ,
308+ bundleId : string ,
309+ referenceDate : Date ,
310+ ) : AppTrends => ( {
311+ daily : computeTrend (
312+ history ,
313+ currentRank ,
314+ bundleId ,
315+ "overall" ,
316+ 1 ,
317+ referenceDate ,
318+ ) ,
319+ weekly : computeTrend (
320+ history ,
321+ currentRank ,
322+ bundleId ,
323+ "overall" ,
324+ 7 ,
325+ referenceDate ,
326+ ) ,
327+ monthly : computeTrend (
328+ history ,
329+ currentRank ,
330+ bundleId ,
331+ "overall" ,
332+ 30 ,
333+ referenceDate ,
334+ ) ,
335+ } ) ;
336+
337+ /** Build Finance-category BriefingItems — only apps ranked in Finance, sorted by rank. */
338+ const buildFinanceItems = (
316339 snapshot : DailySnapshot ,
317340 history : RankingsHistory ,
318341 referenceDate : Date ,
319342) : readonly BriefingItem [ ] => {
320- // Create items with app reference for sorting
321- interface ItemWithRanking {
322- readonly app : TrackedApp ;
323- readonly ranking : DailyAppRanking ;
343+ interface ItemWithRank {
344+ readonly rank : number ;
324345 readonly item : BriefingItem ;
325346 }
326347
327- const itemsWithApps : ItemWithRanking [ ] = TRACKED_APPS . map ( ( app ) => {
348+ return TRACKED_APPS . flatMap ( ( app ) : ItemWithRank [ ] => {
328349 const ranking = snapshot [ app . bundleId ] ?? { overall : null , finance : null } ;
350+ if ( ranking . finance === null ) return [ ] ;
351+
329352 const trends = computeAppTrends (
330353 history ,
331354 ranking . finance ,
332355 app . bundleId ,
333356 referenceDate ,
334357 ) ;
358+ const trendLine = formatTrendLine ( trends ) ;
359+ const text = trendLine
360+ ? `${ formatFinancePositionText ( app , ranking . finance ) } (${ trendLine } )`
361+ : formatFinancePositionText ( app , ranking . finance ) ;
362+
363+ return [
364+ {
365+ rank : ranking . finance ,
366+ item : { text, sentiment : getSentiment ( trends ) , sentimentPrefix : true } ,
367+ } ,
368+ ] ;
369+ } )
370+ . sort ( ( a , b ) => a . rank - b . rank )
371+ . map ( ( { item } ) => item ) ;
372+ } ;
373+
374+ /** Build Overall/Total BriefingItems — only apps ranked in the overall top 100, sorted by rank. */
375+ const buildOverallItems = (
376+ snapshot : DailySnapshot ,
377+ history : RankingsHistory ,
378+ referenceDate : Date ,
379+ ) : readonly BriefingItem [ ] => {
380+ interface ItemWithRank {
381+ readonly rank : number ;
382+ readonly item : BriefingItem ;
383+ }
384+
385+ return TRACKED_APPS . flatMap ( ( app ) : ItemWithRank [ ] => {
386+ const ranking = snapshot [ app . bundleId ] ?? { overall : null , finance : null } ;
387+ if ( ranking . overall === null ) return [ ] ;
335388
389+ const trends = computeOverallAppTrends (
390+ history ,
391+ ranking . overall ,
392+ app . bundleId ,
393+ referenceDate ,
394+ ) ;
336395 const trendLine = formatTrendLine ( trends ) ;
337- return {
338- app,
339- ranking ,
340- item : {
341- text : trendLine
342- ? ` ${ formatPositionText ( app , ranking ) } ( ${ trendLine } )`
343- : formatPositionText ( app , ranking ) ,
344- sentiment : getSentiment ( trends ) ,
396+ const text = trendLine
397+ ? ` ${ formatOverallPositionText ( app , ranking . overall ) } ( ${ trendLine } )`
398+ : formatOverallPositionText ( app , ranking . overall ) ;
399+
400+ return [
401+ {
402+ rank : ranking . overall ,
403+ item : { text , sentiment : getSentiment ( trends ) , sentimentPrefix : true } ,
345404 } ,
346- } ;
347- } ) ;
348-
349- // Sort by best (lowest) position first: Overall preferred, then finance category, unranked last
350- return itemsWithApps
351- . sort ( ( a : ItemWithRanking , b : ItemWithRanking ) => {
352- const aRank =
353- a . ranking . overall ?? a . ranking . finance ?? Number . MAX_SAFE_INTEGER ;
354- const bRank =
355- b . ranking . overall ?? b . ranking . finance ?? Number . MAX_SAFE_INTEGER ;
356- return aRank - bRank ;
357- } )
358- . map ( ( { item } : ItemWithRanking ) => item ) ;
405+ ] ;
406+ } )
407+ . sort ( ( a , b ) => a . rank - b . rank )
408+ . map ( ( { item } ) => item ) ;
359409} ;
360410
361411// ============================================================================
@@ -367,7 +417,7 @@ export const appStoreRankingsSource: DataSource = {
367417 priority : 7 ,
368418 timeoutMs : 30_000 ,
369419
370- fetch : async ( ) : Promise < BriefingSection > => {
420+ fetch : async ( ) : Promise < BriefingSection [ ] > => {
371421 console . log ( "[appstore-rankings] Starting App Store rankings fetch..." ) ;
372422
373423 // Fetch current rankings from both APIs
@@ -378,7 +428,8 @@ export const appStoreRankingsSource: DataSource = {
378428
379429 // Build items with trend data from history
380430 const today = new Date ( ) ;
381- const items = buildBriefingItems ( snapshot , history , today ) ;
431+ const financeItems = buildFinanceItems ( snapshot , history , today ) ;
432+ const overallItems = buildOverallItems ( snapshot , history , today ) ;
382433
383434 // Save today's snapshot to history (for future trend calculations).
384435 // Failure to save is non-fatal: the current briefing data is still valid,
@@ -396,11 +447,24 @@ export const appStoreRankingsSource: DataSource = {
396447 ) ;
397448 }
398449
399- return {
400- title : "App Store Rankings" ,
401- icon : "📱" ,
402- items,
403- } ;
450+ const sections : BriefingSection [ ] = [
451+ {
452+ title : "App Store · Finance" ,
453+ icon : "📱" ,
454+ items : financeItems ,
455+ } ,
456+ ] ;
457+
458+ // Only include Total section if at least one app cracked the overall top 100
459+ if ( overallItems . length > 0 ) {
460+ sections . push ( {
461+ title : "App Store · Total" ,
462+ icon : "📱" ,
463+ items : overallItems ,
464+ } ) ;
465+ }
466+
467+ return sections ;
404468 } ,
405469} ;
406470
@@ -412,24 +476,30 @@ export const mockAppStoreRankingsSource: DataSource = {
412476 name : "App Store Rankings" ,
413477 priority : 7 ,
414478
415- fetch : async ( ) : Promise < BriefingSection > => ( {
416- title : "App Store Rankings" ,
417- icon : "📱" ,
418- items : [
419- {
420- text : "Coinbase: #35 overall · #12 Finance (↑5 daily · ↑12 weekly · ↑25 monthly)" ,
421- sentiment : "positive" ,
422- } ,
423- {
424- text : "Polymarket: #128 Finance (↓46 daily · ↓42 weekly)" ,
425- sentiment : "negative" ,
426- } ,
427- {
428- text : "Kraken: unranked" ,
429- } ,
430- {
431- text : "Crypto.com: unranked" ,
432- } ,
433- ] ,
434- } ) ,
479+ fetch : async ( ) : Promise < BriefingSection [ ] > => [
480+ {
481+ title : "App Store · Finance" ,
482+ icon : "📱" ,
483+ items : [
484+ {
485+ text : "Coinbase: #12 (↑5 daily · ↑12 weekly · ↑25 monthly)" ,
486+ sentiment : "positive" ,
487+ } ,
488+ {
489+ text : "Polymarket: #128 (↓46 daily · ↓42 weekly)" ,
490+ sentiment : "negative" ,
491+ } ,
492+ ] ,
493+ } ,
494+ {
495+ title : "App Store · Total" ,
496+ icon : "📱" ,
497+ items : [
498+ {
499+ text : "Coinbase: #35 (↑2 daily)" ,
500+ sentiment : "positive" ,
501+ } ,
502+ ] ,
503+ } ,
504+ ] ,
435505} ;
0 commit comments