Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

GateStatusScreen.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

GateStatusScreen.tsxBlame352 lines · 1 contributor
9a8fbedClaude1import React, { useCallback } from 'react';
2import {
3 FlatList,
4 ScrollView,
5 StyleSheet,
6 Text,
7 TouchableOpacity,
8 View,
9 ActivityIndicator,
10} from 'react-native';
11import { SafeAreaView } from 'react-native-safe-area-context';
12import type { NativeStackScreenProps } from '@react-navigation/native-stack';
13import { useGates } from '../hooks/useGates';
14import { GateStatusBadge } from '../components/GateStatusBadge';
15import { LoadingSpinner } from '../components/LoadingSpinner';
16import { ErrorBanner } from '../components/ErrorBanner';
17import { colors } from '../theme/colors';
18import type { RepoStackParamList } from '../navigation/AppNavigator';
19import type { GateRun } from '../api/gates';
20
21type Props = NativeStackScreenProps<RepoStackParamList, 'GateStatus'>;
22
23function formatRelative(dateStr: string): string {
24 const diff = Date.now() - new Date(dateStr).getTime();
25 const mins = Math.floor(diff / 60000);
26 if (mins < 1) return 'just now';
27 if (mins < 60) return `${mins}m ago`;
28 const hrs = Math.floor(mins / 60);
29 if (hrs < 24) return `${hrs}h ago`;
30 const days = Math.floor(hrs / 24);
31 if (days < 30) return `${days}d ago`;
32 const months = Math.floor(days / 30);
33 if (months < 12) return `${months}mo ago`;
34 return `${Math.floor(months / 12)}y ago`;
35}
36
37function duration(start: string, end: string | null): string {
38 if (!end) return 'running...';
39 const ms = new Date(end).getTime() - new Date(start).getTime();
40 const secs = Math.round(ms / 1000);
41 if (secs < 60) return `${secs}s`;
42 return `${Math.floor(secs / 60)}m ${secs % 60}s`;
43}
44
45function GateRunCard({ run }: { run: GateRun }): React.ReactElement {
46 const [expanded, setExpanded] = React.useState(false);
47
48 return (
49 <View style={styles.runCard}>
50 <View style={styles.runHeader}>
51 <GateStatusBadge status={run.status} aiRepaired={run.aiRepaired} size="small" />
52 <View style={styles.runMeta}>
53 <Text style={styles.runBranch}>{run.branch}</Text>
54 <Text style={styles.runSha}>{run.commitSha.slice(0, 7)}</Text>
55 </View>
56 <Text style={styles.runTime}>{formatRelative(run.createdAt)}</Text>
57 </View>
58
59 <View style={styles.runDetails}>
60 <Text style={styles.runDetailText}>
61 Duration: {duration(run.startedAt, run.completedAt)}
62 </Text>
63 {run.triggeredBy !== null && (
64 <Text style={styles.runDetailText}>Triggered by {run.triggeredBy}</Text>
65 )}
66 {run.aiRepaired && run.repairedCommitSha !== null && (
67 <View style={styles.repairedRow}>
68 <Text style={styles.sparkle}>✦</Text>
69 <Text style={styles.repairedText}>
70 AI auto-repaired → {run.repairedCommitSha.slice(0, 7)}
71 </Text>
72 </View>
73 )}
74 </View>
75
76 {run.output !== null && run.output.length > 0 && (
77 <TouchableOpacity
78 style={styles.outputToggle}
79 onPress={() => setExpanded((v) => !v)}
80 activeOpacity={0.8}
81 >
82 <Text style={styles.outputToggleText}>
83 {expanded ? '▾ Hide output' : '▸ Show output'}
84 </Text>
85 </TouchableOpacity>
86 )}
87
88 {expanded && run.output !== null && (
89 <ScrollView horizontal showsHorizontalScrollIndicator={false}>
90 <View style={styles.outputBox}>
91 <Text style={styles.outputText} selectable>
92 {run.output}
93 </Text>
94 </View>
95 </ScrollView>
96 )}
97 </View>
98 );
99}
100
101export function GateStatusScreen({ route }: Props): React.ReactElement {
102 const { owner, repo } = route.params;
103 const { runs, isLoading, error, isTriggering, triggerRun, loadMore, hasMore, refresh } =
104 useGates(owner, repo);
105
106 const renderRun = useCallback(
107 ({ item }: { item: GateRun }) => <GateRunCard run={item} />,
108 [],
109 );
110
111 const keyExtractor = useCallback((item: GateRun) => item.id, []);
112
113 const renderFooter = useCallback(
114 () => (hasMore ? <LoadingSpinner size="small" /> : null),
115 [hasMore],
116 );
117
118 // Summary stats
119 const passed = runs.filter((r) => r.status === 'passed').length;
120 const failed = runs.filter((r) => r.status === 'failed').length;
121 const repaired = runs.filter((r) => r.aiRepaired).length;
122
123 if (isLoading && runs.length === 0) return <LoadingSpinner fullScreen />;
124
125 return (
126 <SafeAreaView style={styles.safe} edges={['bottom']}>
127 {error !== null && <ErrorBanner message={error} onRetry={refresh} />}
128
129 <FlatList
130 data={runs}
131 keyExtractor={keyExtractor}
132 renderItem={renderRun}
133 onEndReached={loadMore}
134 onEndReachedThreshold={0.3}
135 ListFooterComponent={renderFooter}
136 contentContainerStyle={styles.listContent}
137 ListHeaderComponent={
138 <View>
139 {/* Stats banner */}
140 <View style={styles.statsBanner}>
141 <View style={styles.statItem}>
142 <Text style={[styles.statValue, { color: colors.accent }]}>{passed}</Text>
143 <Text style={styles.statLabel}>Passed</Text>
144 </View>
145 <View style={[styles.statItem, styles.statItemBorder]}>
146 <Text style={[styles.statValue, { color: colors.accentRed }]}>{failed}</Text>
147 <Text style={styles.statLabel}>Failed</Text>
148 </View>
149 <View style={styles.statItem}>
150 <Text style={[styles.statValue, { color: colors.accentPurple }]}>{repaired}</Text>
151 <Text style={styles.statLabel}>AI Repaired</Text>
152 </View>
153 </View>
154
155 {/* Trigger button */}
156 <View style={styles.triggerSection}>
157 <TouchableOpacity
158 style={[styles.triggerButton, isTriggering && styles.triggerButtonDisabled]}
159 onPress={triggerRun}
160 disabled={isTriggering}
161 activeOpacity={0.8}
162 >
163 {isTriggering ? (
164 <ActivityIndicator size="small" color={colors.text} />
165 ) : (
166 <Text style={styles.triggerText}>⚡ Run Gate Check</Text>
167 )}
168 </TouchableOpacity>
169 </View>
170
171 {runs.length > 0 && (
172 <View style={styles.sectionHeader}>
173 <Text style={styles.sectionTitle}>Gate Run History</Text>
174 </View>
175 )}
176 </View>
177 }
178 ListEmptyComponent={
179 <View style={styles.emptyState}>
180 <Text style={styles.emptyText}>No gate runs yet</Text>
181 <Text style={styles.emptySubText}>
182 Gate checks run automatically on every push to the default branch.
183 </Text>
184 </View>
185 }
186 />
187 </SafeAreaView>
188 );
189}
190
191const styles = StyleSheet.create({
192 safe: {
193 flex: 1,
194 backgroundColor: colors.bg,
195 },
196 listContent: {
197 paddingBottom: 24,
198 },
199 statsBanner: {
200 flexDirection: 'row',
201 backgroundColor: colors.bgSecondary,
202 borderBottomWidth: 1,
203 borderBottomColor: colors.border,
204 },
205 statItem: {
206 flex: 1,
207 paddingVertical: 16,
208 alignItems: 'center',
209 gap: 4,
210 },
211 statItemBorder: {
212 borderLeftWidth: 1,
213 borderRightWidth: 1,
214 borderColor: colors.border,
215 },
216 statValue: {
217 fontSize: 24,
218 fontWeight: '700',
219 },
220 statLabel: {
221 fontSize: 11,
222 color: colors.textMuted,
223 },
224 triggerSection: {
225 padding: 16,
226 borderBottomWidth: 1,
227 borderBottomColor: colors.border,
228 },
229 triggerButton: {
230 backgroundColor: colors.bgTertiary,
231 borderRadius: 8,
232 paddingVertical: 12,
233 alignItems: 'center',
234 borderWidth: 1,
235 borderColor: colors.accentBlue,
236 minHeight: 44,
237 justifyContent: 'center',
238 },
239 triggerButtonDisabled: {
240 opacity: 0.5,
241 },
242 triggerText: {
243 fontSize: 14,
244 fontWeight: '600',
245 color: colors.accentBlue,
246 },
247 sectionHeader: {
248 paddingHorizontal: 16,
249 paddingVertical: 10,
250 },
251 sectionTitle: {
252 fontSize: 12,
253 fontWeight: '600',
254 color: colors.textMuted,
255 textTransform: 'uppercase',
256 letterSpacing: 0.5,
257 },
258 runCard: {
259 backgroundColor: colors.bgSecondary,
260 borderBottomWidth: 1,
261 borderBottomColor: colors.border,
262 marginHorizontal: 16,
263 marginBottom: 10,
264 borderRadius: 8,
265 borderWidth: 1,
266 overflow: 'hidden',
267 },
268 runHeader: {
269 flexDirection: 'row',
270 alignItems: 'center',
271 padding: 12,
272 gap: 10,
273 },
274 runMeta: {
275 flex: 1,
276 gap: 2,
277 },
278 runBranch: {
279 fontSize: 13,
280 fontFamily: 'monospace',
281 color: colors.text,
282 },
283 runSha: {
284 fontSize: 11,
285 fontFamily: 'monospace',
286 color: colors.textMuted,
287 },
288 runTime: {
289 fontSize: 12,
290 color: colors.textMuted,
291 },
292 runDetails: {
293 paddingHorizontal: 12,
294 paddingBottom: 10,
295 gap: 3,
296 },
297 runDetailText: {
298 fontSize: 12,
299 color: colors.textMuted,
300 },
301 repairedRow: {
302 flexDirection: 'row',
303 alignItems: 'center',
304 gap: 5,
305 marginTop: 4,
306 },
307 sparkle: {
308 fontSize: 12,
309 color: colors.accentPurple,
310 },
311 repairedText: {
312 fontSize: 12,
313 color: colors.accentPurple,
314 fontFamily: 'monospace',
315 },
316 outputToggle: {
317 paddingHorizontal: 12,
318 paddingVertical: 8,
319 borderTopWidth: 1,
320 borderTopColor: colors.border,
321 },
322 outputToggleText: {
323 fontSize: 12,
324 color: colors.textLink,
325 },
326 outputBox: {
327 padding: 12,
328 backgroundColor: colors.bg,
329 },
330 outputText: {
331 fontSize: 11,
332 fontFamily: 'monospace',
333 color: colors.text,
334 lineHeight: 16,
335 },
336 emptyState: {
337 padding: 40,
338 alignItems: 'center',
339 gap: 8,
340 },
341 emptyText: {
342 fontSize: 16,
343 fontWeight: '600',
344 color: colors.textMuted,
345 },
346 emptySubText: {
347 fontSize: 13,
348 color: colors.textMuted,
349 textAlign: 'center',
350 lineHeight: 18,
351 },
352});