@@ -92,6 +92,154 @@ const SEVERITY_RANK_EXPR = `MAX(CASE a.severity
9292 WHEN 'LOW' THEN 1
9393 ELSE 0 END)::int`
9494
95+ export interface PackageStatusCounts {
96+ all : number
97+ unassigned : number
98+ open : number
99+ assessing : number
100+ active : number
101+ needs_attention : number
102+ escalated : number
103+ blocked : number
104+ inactive : number
105+ }
106+
107+ export type StatusCountsOptions = Omit <
108+ ListPackagesOptions ,
109+ 'status' | 'page' | 'pageSize' | 'sortBy' | 'sortDir'
110+ >
111+
112+ const ALL_STEWARDSHIP_STATUSES = [
113+ 'unassigned' ,
114+ 'open' ,
115+ 'assessing' ,
116+ 'active' ,
117+ 'needs_attention' ,
118+ 'escalated' ,
119+ 'blocked' ,
120+ 'inactive' ,
121+ ] as const
122+
123+ /**
124+ * Returns per-status counts for the same filter set used by listPackagesForApi, but without
125+ * the status filter so the tab bar always shows all status breakdowns for the active filters.
126+ */
127+ export async function getPackageStatusCounts (
128+ qx : QueryExecutor ,
129+ opts : StatusCountsOptions ,
130+ ) : Promise < PackageStatusCounts > {
131+ const conditions : string [ ] = [ 'p.is_critical = true' ]
132+ const params : Record < string , unknown > = { }
133+
134+ if ( opts . ecosystem ) {
135+ conditions . push ( 'p.ecosystem = $(ecosystem)' )
136+ params . ecosystem = opts . ecosystem
137+ }
138+
139+ if ( opts . name ) {
140+ conditions . push ( 'p.name ILIKE $(name)' )
141+ params . name = `%${ opts . name } %`
142+ }
143+
144+ if ( opts . lifecycle ) {
145+ conditions . push ( 'p.status IS NOT NULL' )
146+ }
147+
148+ if ( opts . healthBand ) {
149+ if ( opts . healthBand === 'healthy' ) {
150+ conditions . push ( 'r_sc.scorecard_score >= 7.0' )
151+ } else if ( opts . healthBand === 'fair' ) {
152+ conditions . push ( 'r_sc.scorecard_score >= 5.0 AND r_sc.scorecard_score < 7.0' )
153+ } else if ( opts . healthBand === 'concerning' ) {
154+ conditions . push ( 'r_sc.scorecard_score >= 3.0 AND r_sc.scorecard_score < 5.0' )
155+ } else {
156+ conditions . push ( '(r_sc.scorecard_score IS NULL OR r_sc.scorecard_score < 3.0)' )
157+ }
158+ }
159+
160+ if ( opts . vulnSeverity ) {
161+ if ( opts . vulnSeverity === 'any' ) {
162+ conditions . push ( 'ap_counts.cnt > 0' )
163+ } else if ( opts . vulnSeverity === 'high' ) {
164+ conditions . push ( 'ap_severity.max_rank >= 3' )
165+ } else {
166+ conditions . push ( 'ap_severity.max_rank >= 4' )
167+ }
168+ }
169+
170+ if ( opts . staleOnly ) {
171+ conditions . push (
172+ `(p.latest_release_at IS NULL OR p.latest_release_at < NOW() - INTERVAL '${ STALE_MONTHS } months')` ,
173+ )
174+ }
175+
176+ if ( opts . unstewardedOnly ) {
177+ conditions . push ( `(s.status = 'unassigned' OR s.id IS NULL)` )
178+ }
179+
180+ if ( opts . busFactor1Only ) {
181+ conditions . push ( `pm_counts.cnt = 1` )
182+ }
183+
184+ const where = `WHERE ${ conditions . join ( ' AND ' ) } `
185+
186+ const rows : { status : string ; count : number } [ ] = await qx . select (
187+ `
188+ SELECT
189+ COALESCE(s.status, 'unassigned') AS status,
190+ COUNT(*)::int AS count
191+ FROM packages p
192+ LEFT JOIN stewardships s ON s.package_id = p.id
193+ LEFT JOIN LATERAL (
194+ SELECT COUNT(*)::int AS cnt FROM advisory_packages WHERE package_id = p.id
195+ ) ap_counts ON true
196+ LEFT JOIN LATERAL (
197+ SELECT COUNT(*)::int AS cnt FROM package_maintainers pm WHERE pm.package_id = p.id
198+ ) pm_counts ON true
199+ LEFT JOIN LATERAL (
200+ SELECT ${ SEVERITY_RANK_EXPR } AS max_rank
201+ FROM advisory_packages ap
202+ JOIN advisories a ON a.id = ap.advisory_id
203+ WHERE ap.package_id = p.id
204+ ) ap_severity ON true
205+ LEFT JOIN LATERAL (
206+ SELECT r.scorecard_score
207+ FROM package_repos pr
208+ JOIN repos r ON r.id = pr.repo_id
209+ WHERE pr.package_id = p.id
210+ ORDER BY pr.confidence DESC
211+ LIMIT 1
212+ ) r_sc ON true
213+ ${ where }
214+ GROUP BY COALESCE(s.status, 'unassigned')
215+ ` ,
216+ params ,
217+ )
218+
219+ const countsMap : Record < string , number > = { }
220+ let all = 0
221+ for ( const row of rows ) {
222+ countsMap [ row . status ] = row . count
223+ all += row . count
224+ }
225+
226+ const result : PackageStatusCounts = {
227+ all,
228+ unassigned : 0 ,
229+ open : 0 ,
230+ assessing : 0 ,
231+ active : 0 ,
232+ needs_attention : 0 ,
233+ escalated : 0 ,
234+ blocked : 0 ,
235+ inactive : 0 ,
236+ }
237+ for ( const status of ALL_STEWARDSHIP_STATUSES ) {
238+ result [ status ] = countsMap [ status ] ?? 0
239+ }
240+ return result
241+ }
242+
95243export async function listPackagesForApi (
96244 qx : QueryExecutor ,
97245 opts : ListPackagesOptions ,
0 commit comments