55 */
66
77import { getStateManager , PRMonitor , IssueConversationMonitor } from '../core/index.js' ;
8- import { errorMessage , getHttpStatusCode } from '../core/errors.js' ;
8+ import { errorMessage , isRateLimitOrAuthError } from '../core/errors.js' ;
9+ import { warn } from '../core/logger.js' ;
910import { emptyPRCountsResult } from '../core/github-stats.js' ;
1011import { toShelvedPRRef } from './daily.js' ;
12+
13+ const MODULE = 'dashboard-data' ;
1114import type { DailyDigest , AgentState , ClosedPR , MergedPR , CommentedIssue } from '../core/types.js' ;
1215
1316export interface DashboardStats {
@@ -34,6 +37,53 @@ export function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentSt
3437 } ;
3538}
3639
40+ /**
41+ * Persist monthly chart analytics (merged, closed, opened) to state.
42+ * Each metric is isolated so partial failures don't produce inconsistent state.
43+ * Skips overwriting when data is empty to avoid wiping chart data on transient API failures.
44+ */
45+ export function updateMonthlyAnalytics (
46+ prs : Array < { createdAt ?: string } > ,
47+ monthlyCounts : Record < string , number > ,
48+ monthlyClosedCounts : Record < string , number > ,
49+ openedFromMerged : Record < string , number > ,
50+ openedFromClosed : Record < string , number > ,
51+ ) : void {
52+ const stateManager = getStateManager ( ) ;
53+
54+ try {
55+ if ( Object . keys ( monthlyCounts ) . length > 0 ) {
56+ stateManager . setMonthlyMergedCounts ( monthlyCounts ) ;
57+ }
58+ } catch ( error ) {
59+ warn ( MODULE , `Failed to store monthly merged counts: ${ errorMessage ( error ) } ` ) ;
60+ }
61+ try {
62+ if ( Object . keys ( monthlyClosedCounts ) . length > 0 ) {
63+ stateManager . setMonthlyClosedCounts ( monthlyClosedCounts ) ;
64+ }
65+ } catch ( error ) {
66+ warn ( MODULE , `Failed to store monthly closed counts: ${ errorMessage ( error ) } ` ) ;
67+ }
68+ try {
69+ const combinedOpenedCounts : Record < string , number > = { ...openedFromMerged } ;
70+ for ( const [ month , count ] of Object . entries ( openedFromClosed ) ) {
71+ combinedOpenedCounts [ month ] = ( combinedOpenedCounts [ month ] || 0 ) + count ;
72+ }
73+ for ( const pr of prs ) {
74+ if ( pr . createdAt ) {
75+ const month = pr . createdAt . slice ( 0 , 7 ) ;
76+ combinedOpenedCounts [ month ] = ( combinedOpenedCounts [ month ] || 0 ) + 1 ;
77+ }
78+ }
79+ if ( Object . keys ( combinedOpenedCounts ) . length > 0 ) {
80+ stateManager . setMonthlyOpenedCounts ( combinedOpenedCounts ) ;
81+ }
82+ } catch ( error ) {
83+ warn ( MODULE , `Failed to store monthly opened counts: ${ errorMessage ( error ) } ` ) ;
84+ }
85+ }
86+
3787export interface DashboardFetchResult {
3888 digest : DailyDigest ;
3989 commentedIssues : CommentedIssue [ ] ;
@@ -44,16 +94,6 @@ export interface DashboardFetchResult {
4494 * Returns the digest and commented issues, updating state as a side effect.
4595 * Throws if the fetch fails entirely (caller should fall back to cached data).
4696 */
47- function isRateLimitOrAuthError ( err : unknown ) : boolean {
48- const status = getHttpStatusCode ( err ) ;
49- if ( status === 401 || status === 429 ) return true ;
50- if ( status === 403 ) {
51- const msg = errorMessage ( err ) . toLowerCase ( ) ;
52- return msg . includes ( 'rate limit' ) || msg . includes ( 'abuse detection' ) ;
53- }
54- return false ;
55- }
56-
5797export async function fetchDashboardData ( token : string ) : Promise < DashboardFetchResult > {
5898 const stateManager = getStateManager ( ) ;
5999 const prMonitor = new PRMonitor ( token ) ;
@@ -64,30 +104,30 @@ export async function fetchDashboardData(token: string): Promise<DashboardFetchR
64104 prMonitor . fetchUserOpenPRs ( ) ,
65105 prMonitor . fetchRecentlyClosedPRs ( ) . catch ( ( err ) : ClosedPR [ ] => {
66106 if ( isRateLimitOrAuthError ( err ) ) throw err ;
67- console . error ( `Warning: Failed to fetch recently closed PRs: ${ errorMessage ( err ) } `) ;
107+ warn ( MODULE , ` Failed to fetch recently closed PRs: ${ errorMessage ( err ) } `) ;
68108 return [ ] ;
69109 } ) ,
70110 prMonitor . fetchRecentlyMergedPRs ( ) . catch ( ( err ) : MergedPR [ ] => {
71111 if ( isRateLimitOrAuthError ( err ) ) throw err ;
72- console . error ( `Warning: Failed to fetch recently merged PRs: ${ errorMessage ( err ) } `) ;
112+ warn ( MODULE , ` Failed to fetch recently merged PRs: ${ errorMessage ( err ) } `) ;
73113 return [ ] ;
74114 } ) ,
75115 prMonitor . fetchUserMergedPRCounts ( ) . catch ( ( err ) => {
76116 if ( isRateLimitOrAuthError ( err ) ) throw err ;
77- console . error ( `Warning: Failed to fetch merged PR counts: ${ errorMessage ( err ) } `) ;
117+ warn ( MODULE , ` Failed to fetch merged PR counts: ${ errorMessage ( err ) } `) ;
78118 return emptyPRCountsResult < { count : number ; lastMergedAt : string } > ( ) ;
79119 } ) ,
80120 prMonitor . fetchUserClosedPRCounts ( ) . catch ( ( err ) => {
81121 if ( isRateLimitOrAuthError ( err ) ) throw err ;
82- console . error ( `Warning: Failed to fetch closed PR counts: ${ errorMessage ( err ) } `) ;
122+ warn ( MODULE , ` Failed to fetch closed PR counts: ${ errorMessage ( err ) } `) ;
83123 return emptyPRCountsResult < number > ( ) ;
84124 } ) ,
85125 issueMonitor . fetchCommentedIssues ( ) . catch ( ( error ) => {
86126 const msg = errorMessage ( error ) ;
87127 if ( msg . includes ( 'No GitHub username configured' ) ) {
88- console . error ( `[DASHBOARD] Issue conversation tracking requires setup: ${ msg } `) ;
128+ warn ( MODULE , ` Issue conversation tracking requires setup: ${ msg } `) ;
89129 } else {
90- console . error ( `[DASHBOARD] Issue conversation fetch failed: ${ msg } `) ;
130+ warn ( MODULE , ` Issue conversation fetch failed: ${ msg } `) ;
91131 }
92132 return {
93133 issues : [ ] as CommentedIssue [ ] ,
@@ -98,49 +138,17 @@ export async function fetchDashboardData(token: string): Promise<DashboardFetchR
98138
99139 const commentedIssues = fetchedIssues . issues ;
100140 if ( fetchedIssues . failures . length > 0 ) {
101- console . error ( `[DASHBOARD] ${ fetchedIssues . failures . length } issue conversation check(s) failed`) ;
141+ warn ( MODULE , ` ${ fetchedIssues . failures . length } issue conversation check(s) failed`) ;
102142 }
103143
104144 if ( failures . length > 0 ) {
105- console . error ( `Warning: ${ failures . length } PR fetch(es) failed`) ;
145+ warn ( MODULE , ` ${ failures . length } PR fetch(es) failed`) ;
106146 }
107147
108148 // Store monthly chart data (opened/merged/closed) so charts have data
109149 const { monthlyCounts, monthlyOpenedCounts : openedFromMerged } = mergedResult ;
110150 const { monthlyCounts : monthlyClosedCounts , monthlyOpenedCounts : openedFromClosed } = closedResult ;
111-
112- // Guard: skip overwriting when data is empty to avoid wiping chart data on transient API failures.
113- try {
114- if ( Object . keys ( monthlyCounts ) . length > 0 ) {
115- stateManager . setMonthlyMergedCounts ( monthlyCounts ) ;
116- }
117- } catch ( error ) {
118- console . error ( '[DASHBOARD] Failed to store monthly merged counts:' , errorMessage ( error ) ) ;
119- }
120- try {
121- if ( Object . keys ( monthlyClosedCounts ) . length > 0 ) {
122- stateManager . setMonthlyClosedCounts ( monthlyClosedCounts ) ;
123- }
124- } catch ( error ) {
125- console . error ( '[DASHBOARD] Failed to store monthly closed counts:' , errorMessage ( error ) ) ;
126- }
127- try {
128- const combinedOpenedCounts : Record < string , number > = { ...openedFromMerged } ;
129- for ( const [ month , count ] of Object . entries ( openedFromClosed ) ) {
130- combinedOpenedCounts [ month ] = ( combinedOpenedCounts [ month ] || 0 ) + count ;
131- }
132- for ( const pr of prs ) {
133- if ( pr . createdAt ) {
134- const month = pr . createdAt . slice ( 0 , 7 ) ;
135- combinedOpenedCounts [ month ] = ( combinedOpenedCounts [ month ] || 0 ) + 1 ;
136- }
137- }
138- if ( Object . keys ( combinedOpenedCounts ) . length > 0 ) {
139- stateManager . setMonthlyOpenedCounts ( combinedOpenedCounts ) ;
140- }
141- } catch ( error ) {
142- console . error ( '[DASHBOARD] Failed to store monthly opened counts:' , errorMessage ( error ) ) ;
143- }
151+ updateMonthlyAnalytics ( prs , monthlyCounts , monthlyClosedCounts , openedFromMerged , openedFromClosed ) ;
144152
145153 const digest = prMonitor . generateDigest ( prs , recentlyClosedPRs , recentlyMergedPRs ) ;
146154
@@ -156,9 +164,9 @@ export async function fetchDashboardData(token: string): Promise<DashboardFetchR
156164 try {
157165 stateManager . save ( ) ;
158166 } catch ( error ) {
159- console . error ( 'Warning: Failed to save dashboard digest to state:' , errorMessage ( error ) ) ;
167+ warn ( MODULE , ` Failed to save dashboard digest to state: ${ errorMessage ( error ) } ` ) ;
160168 }
161- console . error ( `Refreshed: ${ prs . length } PRs fetched` ) ;
169+ warn ( MODULE , `Refreshed: ${ prs . length } PRs fetched` ) ;
162170
163171 return { digest, commentedIssues } ;
164172}
0 commit comments