11/**
2- * InstallationDetail — session history list for a single installation.
2+ * InstallationDetail — session history for a single installation.
3+ * Matches R2 redesign: completion bar, grid table, filter buttons.
34 */
45
56import { useState , useEffect , useCallback } from 'react'
6- import { useParams , useNavigate , Link } from 'react-router'
7+ import { useParams , useNavigate } from 'react-router'
78import { fetchInstallationSessions } from './dashboardApi'
8- import type { SessionSummary , PaginatedSessionsResponse } from './dashboardApi'
9- import StatusBadge from './StatusBadge'
9+ import type { PaginatedSessionsResponse } from './dashboardApi'
1010import { formatDuration , formatRelativeTime } from './formatters'
11- import { Button , Card , Chip , Dot , Overline } from '../../shared/components'
11+ import { Button , Chip , Dot , Overline } from '../../shared/components'
1212
1313const STATUS_OPTIONS = [ 'all' , 'completed' , 'failed' , 'running' , 'pending' , 'timeout' ] as const
1414
1515export default function InstallationDetail ( ) {
1616 const { installationId } = useParams < { installationId : string } > ( )
1717 const navigate = useNavigate ( )
18- const [ sessions , setSessions ] = useState < SessionSummary [ ] > ( [ ] )
18+ const [ sessions , setSessions ] = useState < PaginatedSessionsResponse [ 'items' ] > ( [ ] )
1919 const [ page , setPage ] = useState ( 1 )
2020 const [ totalPages , setTotalPages ] = useState ( 0 )
2121 const [ total , setTotal ] = useState ( 0 )
@@ -28,7 +28,7 @@ export default function InstallationDetail() {
2828 setLoading ( true )
2929 setError ( null )
3030 try {
31- const data : PaginatedSessionsResponse = await fetchInstallationSessions (
31+ const data = await fetchInstallationSessions (
3232 Number ( installationId ) ,
3333 page ,
3434 20 ,
@@ -51,57 +51,63 @@ export default function InstallationDetail() {
5151 setPage ( 1 )
5252 }
5353
54+ // Completion stats from loaded sessions (approximation from current page)
55+ const completed = sessions . filter ( ( s ) => s . status === 'completed' ) . length
56+ const failed = sessions . filter ( ( s ) => s . status === 'failed' ) . length
57+ const timeout = sessions . filter ( ( s ) => s . status === 'timeout' ) . length
58+ const completionPct = total > 0 ? Math . round ( ( completed / Math . max ( sessions . length , 1 ) ) * 100 ) : 0
59+
5460 return (
5561 < div >
56- { /* Breadcrumb */ }
57- < div className = "flex items-center gap-2 mb-6 text-sm" >
58- < Link to = "/installations" className = "text-dim hover:text-ink2 transition-colors font-mono" >
59- ← Installations
60- </ Link >
61- < span className = "text-dim2" > |</ span >
62- < span className = "text-ink font-mono font-medium" >
63- Installation #{ installationId }
64- </ span >
65- < div className = "ml-auto" >
66- < Link
67- to = { `/installations/${ installationId } /settings` }
68- className = "text-dim text-xs font-mono hover:text-ink2 transition-colors"
69- >
70- Settings
71- </ Link >
72- </ div >
73- </ div >
74-
75- { /* Title + filter */ }
76- < div className = "flex items-center justify-between mb-6" >
62+ { /* Header + filters */ }
63+ < div className = "flex justify-between items-end mb-5" >
7764 < div >
78- < h1 className = "font-mono text-lg font-bold" >
79- Session History
80- { ! loading && (
81- < span className = "text-dim text-sm font-normal ml-2" > ({ total } )</ span >
82- ) }
83- </ h1 >
84- < Overline className = "mt-1" > // every time Claude ran against one of your PRs</ Overline >
65+ < Overline className = "mb-1.5" > { '\u25b8' } SESSIONS { '\u00b7' } { total } TOTAL</ Overline >
66+ < h1 className = "font-mono text-2xl font-bold tracking-[-0.02em]" > Session History</ h1 >
67+ < p className = "font-mono text-sm text-dim mt-1" > // every time Claude ran against one of your PRs</ p >
8568 </ div >
86-
87- < select
88- value = { statusFilter }
89- onChange = { ( e ) => handleStatusChange ( e . target . value ) }
90- className = "font-mono text-xs px-3 py-1.5 rounded-button bg-card border border-rule text-ink cursor-pointer outline-none"
91- >
69+ < div className = "flex gap-2" >
9270 { STATUS_OPTIONS . map ( ( opt ) => (
93- < option key = { opt } value = { opt } className = "bg-card" >
94- { opt . charAt ( 0 ) . toUpperCase ( ) + opt . slice ( 1 ) }
95- </ option >
71+ < button
72+ key = { opt }
73+ onClick = { ( ) => handleStatusChange ( opt ) }
74+ className = { `font-mono text-[13px] font-medium px-4 py-2 rounded-[7px] border transition-colors cursor-pointer ${
75+ statusFilter === opt
76+ ? 'bg-accent/15 border-accent/30 text-accent'
77+ : 'bg-card border-rule-str text-ink hover:bg-card-hi'
78+ } `}
79+ >
80+ { opt === 'all' ? 'Status: all' : opt . charAt ( 0 ) . toUpperCase ( ) + opt . slice ( 1 ) }
81+ </ button >
9682 ) ) }
97- </ select >
83+ </ div >
9884 </ div >
9985
86+ { /* Completion bar */ }
87+ { ! loading && sessions . length > 0 && (
88+ < div className = "flex items-center gap-5 px-4 py-3.5 bg-card border border-rule rounded-card mb-5" >
89+ < div className = "flex items-baseline gap-1.5" >
90+ < span className = "font-mono text-[22px] font-bold" > { completionPct } %</ span >
91+ < span className = "font-mono text-[10px] text-dim tracking-[0.12em] uppercase" > completion</ span >
92+ </ div >
93+ < div className = "flex-1 h-1.5 bg-bg2 rounded-full flex gap-0.5 overflow-hidden" >
94+ { completed > 0 && < div className = "bg-ok" style = { { flex : completed } } /> }
95+ { timeout > 0 && < div className = "bg-warn" style = { { flex : timeout } } /> }
96+ { failed > 0 && < div className = "bg-danger" style = { { flex : failed } } /> }
97+ </ div >
98+ < div className = "flex gap-4 font-mono text-[11px] text-dim" >
99+ < span > < Dot color = "ok" className = "mr-1" /> { completed } done</ span >
100+ < span > < Dot color = "warn" className = "mr-1" /> { timeout } timeout</ span >
101+ < span > < Dot color = "danger" className = "mr-1" /> { failed } failed</ span >
102+ </ div >
103+ </ div >
104+ ) }
105+
100106 { /* Error */ }
101107 { error && (
102- < Card className = "mb-6 border-danger/30 " >
103- < p className = "text-danger text-sm" > { error } </ p >
104- </ Card >
108+ < div className = "mb-4 px-4 py-3 bg-danger/8 border border -danger/20 rounded-card text-danger text-sm " >
109+ { error }
110+ </ div >
105111 ) }
106112
107113 { /* Loading */ }
@@ -113,39 +119,64 @@ export default function InstallationDetail() {
113119
114120 { /* Empty */ }
115121 { ! loading && ! error && sessions . length === 0 && (
116- < Card className = "text-center py-12" >
122+ < div className = "text-center py-12 bg-card border border-rule rounded-card " >
117123 < p className = "text-ink2 text-sm" >
118124 { statusFilter === 'all' ? 'No sessions yet.' : `No ${ statusFilter } sessions.` }
119125 </ p >
120- </ Card >
126+ </ div >
121127 ) }
122128
123129 { /* Session table */ }
124130 { ! loading && sessions . length > 0 && (
125131 < >
126- < div className = "rounded-card border border-rule overflow-hidden" >
127- { sessions . map ( ( session , idx ) => (
128- < button
129- key = { session . id }
130- onClick = { ( ) => navigate ( `/installations/${ installationId } /sessions/${ session . id } ` ) }
131- className = { `w-full text-left px-5 py-3.5 flex items-center gap-4 transition-colors cursor-pointer hover:bg-card-hi ${ idx % 2 === 0 ? 'bg-card/50' : 'bg-transparent' } ${ idx < sessions . length - 1 ? 'border-b border-rule/50' : '' } ` }
132- >
133- < div className = "flex-1 min-w-0" >
134- < span className = "text-ink text-[13px]" > { session . repo_full_name } </ span >
135- < span className = "text-dim text-xs ml-1.5" > #{ session . pr_number } </ span >
136- </ div >
137- < Chip variant = "accent" > { session . skill_name } </ Chip >
138- < div className = "shrink-0 w-20 text-center" >
139- < StatusBadge status = { session . status } />
140- </ div >
141- < span className = "text-dim text-xs font-mono shrink-0 w-16 text-right" >
142- { formatDuration ( session . started_at , session . completed_at ) }
143- </ span >
144- < span className = "text-dim text-xs shrink-0 w-16 text-right" >
145- { formatRelativeTime ( session . created_at ) }
146- </ span >
147- </ button >
148- ) ) }
132+ < div className = "border border-rule-str rounded-card overflow-hidden" >
133+ { /* Header row */ }
134+ < div
135+ className = "grid bg-bg2 px-4 py-2.5 font-mono text-[10px] text-dim tracking-[0.18em] uppercase border-b border-rule"
136+ style = { { gridTemplateColumns : '36px 1fr 140px 110px 80px 80px' } }
137+ >
138+ < span > #</ span >
139+ < span > REPO / PR</ span >
140+ < span > SKILL</ span >
141+ < span > STATUS</ span >
142+ < span className = "text-right" > DURATION</ span >
143+ < span className = "text-right" > RAN</ span >
144+ </ div >
145+
146+ { /* Data rows */ }
147+ { sessions . map ( ( session , idx ) => {
148+ const statusColor =
149+ session . status === 'completed' ? 'ok' as const :
150+ session . status === 'timeout' ? 'warn' as const :
151+ session . status === 'failed' ? 'danger' as const :
152+ 'default' as const
153+
154+ return (
155+ < button
156+ key = { session . id }
157+ onClick = { ( ) => navigate ( `/installations/${ installationId } /sessions/${ session . id } ` ) }
158+ className = { `w-full grid items-center px-4 py-3 text-[13px] transition-colors cursor-pointer hover:bg-card-hi ${
159+ idx < sessions . length - 1 ? 'border-b border-rule' : ''
160+ } `}
161+ style = { { gridTemplateColumns : '36px 1fr 140px 110px 80px 80px' } }
162+ >
163+ < span className = "font-mono text-[11px] text-dim2" >
164+ { String ( idx + 1 + ( page - 1 ) * 20 ) . padStart ( 2 , '0' ) }
165+ </ span >
166+ < span className = "font-mono text-ink truncate" >
167+ { session . repo_full_name } < span className = "text-accent" > #{ session . pr_number } </ span >
168+ </ span >
169+ < span > < Chip variant = "accent" > { session . skill_name } </ Chip > </ span >
170+ < span > < Chip variant = { statusColor } > { session . status } </ Chip > </ span >
171+ < span className = "font-mono text-xs text-ink2 text-right" >
172+ { formatDuration ( session . started_at , session . completed_at ) }
173+ </ span >
174+ < span className = "font-mono text-xs text-dim text-right" >
175+ { formatRelativeTime ( session . created_at ) }
176+ </ span >
177+ </ button >
178+ )
179+ } ) }
149180 </ div >
150181
151182 { /* Pagination */ }
0 commit comments