@@ -6,14 +6,52 @@ import { C, PageTitle, EmptyOk } from '../components/UI'
66const TABS = [
77 { key : 'dead' , label : 'Dead Issues' } ,
88 { key : 'zombie' , label : 'Zombie PRs' } ,
9- { key : 'risky ' , label : 'Risky Repos ' } ,
9+ { key : 'stale ' , label : 'Stale Issues Ratio ' } ,
1010 { key : 'license' , label : 'No License' } ,
1111]
1212
13+ const getStatus = ratio => {
14+ if ( ratio <= 10 )
15+ return {
16+ label : 'Excellent' ,
17+ color : 'var(--green)' ,
18+ bg : 'rgba(34,197,94,.12)'
19+ }
20+
21+ if ( ratio <= 25 )
22+ return {
23+ label : 'Healthy' ,
24+ color : 'var(--amber)' ,
25+ bg : 'rgba(250,204,21,.12)'
26+ }
27+
28+ if ( ratio <= 40 )
29+ return {
30+ label : 'Warning' ,
31+ color : 'var(--orange)' ,
32+ bg : 'rgba(251,146,60,.12)'
33+ }
34+
35+ return {
36+ label : 'Critical' ,
37+ color : 'var(--red)' ,
38+ bg : 'rgba(239,68,68,.12)'
39+ }
40+ }
41+
1342export default function GovernancePage ( ) {
14- const { model, issuesData, runAudit, govLoading } = useApp ( )
43+ const { model, issuesData, runAudit, govLoading, staleRepoStats } = useApp ( )
1544 const [ tab , setTab ] = useState ( 'dead' )
1645
46+ const ITEMS_PER_PAGE = 10
47+ const [ stalePage , setStalePage ] = useState ( 1 )
48+ const totalPages = Math . ceil ( staleRepoStats . length / ITEMS_PER_PAGE )
49+
50+ const paginatedStaleRepos = useMemo ( ( ) => {
51+ const start = ( stalePage - 1 ) * ITEMS_PER_PAGE
52+ return staleRepoStats . slice ( start , start + ITEMS_PER_PAGE )
53+ } , [ staleRepoStats , stalePage ] )
54+ console . log ( paginatedStaleRepos ) ;
1755 // Flatten all issues and tag with repo/org
1856 const allIssues = useMemo ( ( ) => {
1957 const arr = [ ]
@@ -34,28 +72,21 @@ export default function GovernancePage() {
3472 . filter ( i => ! i . pull_request && daysSince ( i . created_at ) >= 90 )
3573 . sort ( ( a , b ) => daysSince ( b . created_at ) - daysSince ( a . created_at ) )
3674
37- // Health check 2 — Zombie PRs (>90 days open)
75+ // Health check 2 — Percentage of dead issues relative to all issues
76+ const staleIssuesRatio = allIssues . length ? ( deadIssues . length / allIssues . length ) * 100 : 0 ;
77+
78+ // Health check 3 — Zombie PRs (>90 days open)
3879 const zombiePRs = allIssues
3980 . filter ( i => i . pull_request && daysSince ( i . created_at ) >= 90 )
4081 . sort ( ( a , b ) => daysSince ( b . created_at ) - daysSince ( a . created_at ) )
4182
42- // Health check 3 — Risky repos (top-2 contributor concentration >80%)
43- const riskyRepos = model . allRepos . filter ( r => {
44- const c = r . contributors || [ ]
45- if ( ! c . length ) return false
46- const total = c . reduce ( ( s , x ) => s + x . contributions , 0 )
47- if ( ! total ) return false
48- const topTwo = ( c [ 0 ] ?. contributions || 0 ) + ( c [ 1 ] ?. contributions || 0 )
49- return topTwo / total > 0.8
50- } )
51-
5283 // Health check 4 — No license
5384 const noLicense = model . allRepos . filter ( r => ! r . license && ! r . archived && ! r . fork )
5485
5586 // Issue resolution rate per repo
5687 const topRepos = model . allRepos . slice ( 0 , 8 )
5788
58- const counts = { dead : deadIssues . length , zombie : zombiePRs . length , risky : riskyRepos . length , license : noLicense . length }
89+ const counts = { dead : deadIssues . length , zombie : zombiePRs . length , license : noLicense . length , stale : staleIssuesRatio . toFixed ( 2 ) }
5990
6091 // Stat card
6192 const StatBox = ( { label, value, sub, color } ) => (
@@ -124,8 +155,8 @@ export default function GovernancePage() {
124155 { /* Summary stat cards */ }
125156 < div style = { { display : 'grid' , gridTemplateColumns : 'repeat(4,1fr)' , gap : 12 , marginBottom : 24 } } >
126157 < StatBox label = "Dead Issues" value = { counts . dead } sub = "OPEN 90+ DAYS" color = "var(--red)" />
158+ < StatBox label = "Stale Issues Ratio" value = { `${ staleIssuesRatio . toFixed ( 2 ) } %` } sub = { `of ${ allIssues . length } total issues` } color = { ` ${ getStatus ( staleIssuesRatio ) . color } ` } />
127159 < StatBox label = "Zombie PRs" value = { counts . zombie } sub = "PENDING 90+ DAYS" color = "var(--amber)" />
128- < StatBox label = "Risky Repos" value = { counts . risky } sub = "TOP-2 CONCENTRATION" color = "var(--amber)" />
129160 < StatBox label = "No License" value = { counts . license } sub = "COMPLIANCE MISSING" color = "var(--text2)" />
130161 </ div >
131162
@@ -182,7 +213,7 @@ export default function GovernancePage() {
182213 } }
183214 >
184215 { t . label } { ' ' }
185- < span style = { { color : counts [ t . key ] > 0 ? 'var(--red)' : 'var(--green)' , marginLeft : 4 } } >
216+ < span style = { { color : counts [ t . key ] > 40 ? 'var(--red)' : 'var(--green)' , marginLeft : 4 } } >
186217 { counts [ t . key ] }
187218 </ span >
188219 </ button >
@@ -213,28 +244,108 @@ export default function GovernancePage() {
213244 ) : < EmptyOk msg = "No zombie PRs found" sub = "This org reviews and closes contributions actively." />
214245 ) }
215246
216- { /* Risky Repos */ }
217- { tab === 'risky' && (
218- riskyRepos . length ? (
219- < div style = { { display : 'flex' , flexDirection : 'column' , gap : 10 } } >
220- { riskyRepos . map ( r => {
221- const c = r . contributors || [ ]
222- const total = c . reduce ( ( s , x ) => s + x . contributions , 0 ) || 1
223- const pct = Math . round ( ( ( c [ 0 ] ?. contributions || 0 ) + ( c [ 1 ] ?. contributions || 0 ) ) / total * 100 )
224- return (
225- < div key = { r . id } style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , padding : '12px 14px' , background : 'var(--surface2)' , borderRadius : 6 } } >
226- < div >
227- < div style = { { fontWeight : 500 , fontSize : 13 } } > { r . name } </ div >
228- < div style = { { fontSize : 12 , color : 'var(--text2)' , marginTop : 2 } } >
229- Top 2 contributors own { pct } % of all commits — concentration risk
247+ { /* Stale Issues */ }
248+ { tab === 'stale' && (
249+ staleRepoStats . length ? (
250+ < >
251+ < div style = { { display : 'flex' , flexDirection : 'column' , gap : 10 } } >
252+ { paginatedStaleRepos . map ( repo => {
253+ const status = getStatus ( repo . ratio )
254+
255+ return (
256+ < div
257+ key = { repo . id }
258+ style = { {
259+ display : 'flex' ,
260+ justifyContent : 'space-between' ,
261+ alignItems : 'center' ,
262+ padding : '12px 14px' ,
263+ background : 'var(--surface2)' ,
264+ borderRadius : 6
265+ } }
266+ >
267+ < div >
268+ < div style = { { fontWeight : 600 } } >
269+ { repo . repo }
270+ </ div >
271+
272+ < div
273+ style = { {
274+ marginTop : 4 ,
275+ fontSize : 12 ,
276+ color : 'var(--text2)'
277+ } }
278+ >
279+ { repo . staleCount } stale issues out of{ ' ' }
280+ { repo . openCount } open issues ({ repo . ratio } %)
281+ </ div >
282+ </ div >
283+
284+ < div
285+ style = { {
286+ display : 'flex' ,
287+ alignItems : 'center' ,
288+ gap : 30
289+ } }
290+ >
291+ < span style = { C . pill ( status . color , status . bg ) } >
292+ { status . label }
293+ </ span >
294+ < a href = { `https://github.com/${ repo . org } /${ repo . repo } /issues` } target = "_blank" rel = "noreferrer"
295+ style = { { fontSize : 12 , color : 'var(--text2)' , display : 'flex' , alignItems : 'center' , gap : 4 } } >
296+ < FiExternalLink size = { 12 } /> GitHub
297+ </ a >
230298 </ div >
231299 </ div >
232- < span style = { C . pill ( 'var(--red)' , 'rgba(239,68,68,.12)' ) } > HIGH RISK</ span >
233- </ div >
234- )
235- } ) }
236- </ div >
237- ) : < EmptyOk msg = "No high-concentration repos found" sub = "Healthy contributor distribution across the portfolio." />
300+ )
301+ } ) }
302+ </ div >
303+
304+ { totalPages > 1 && (
305+ < div
306+ style = { {
307+ display : 'flex' ,
308+ justifyContent : 'space-between' ,
309+ alignItems : 'center' ,
310+ marginTop : 16
311+ } }
312+ >
313+ < button
314+ onClick = { ( ) => setStalePage ( p => p - 1 ) }
315+ disabled = { stalePage === 1 }
316+ style = { {
317+ padding : '8px 14px' ,
318+ cursor : stalePage === 1 ? 'not-allowed' : 'pointer' ,
319+ opacity : stalePage === 1 ? 0.5 : 1
320+ } }
321+ >
322+ ← Previous
323+ </ button >
324+
325+ < span style = { { fontSize : 13 , color : 'var(--text2)' } } >
326+ Page { stalePage } of { totalPages }
327+ </ span >
328+
329+ < button
330+ onClick = { ( ) => setStalePage ( p => p + 1 ) }
331+ disabled = { stalePage === totalPages }
332+ style = { {
333+ padding : '8px 14px' ,
334+ cursor : stalePage === totalPages ? 'not-allowed' : 'pointer' ,
335+ opacity : stalePage === totalPages ? 0.5 : 1
336+ } }
337+ >
338+ Next →
339+ </ button >
340+ </ div >
341+ ) }
342+ </ >
343+ ) : (
344+ < EmptyOk
345+ msg = "No stale issues found"
346+ sub = "All repositories have active open issues."
347+ />
348+ )
238349 ) }
239350
240351 { /* No License */ }
0 commit comments