react-native-inapp-inspector 1.1.35 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +123 -101
  2. package/android/build.gradle +13 -1
  3. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +890 -0
  4. package/android/src/main/java/com/inappinspector/NetworkInspectorPackage.kt +17 -0
  5. package/dist/commonjs/components/ConsoleLogCard.js +5 -5
  6. package/dist/commonjs/components/CopyButton.js +3 -0
  7. package/dist/commonjs/components/EndOfListFooter.js +1 -1
  8. package/dist/commonjs/components/ErrorBoundary.js +3 -0
  9. package/dist/commonjs/components/Inspector/AnalyticsTab.js +25 -73
  10. package/dist/commonjs/components/Inspector/BundleTab.js +671 -189
  11. package/dist/commonjs/components/Inspector/ConsoleTab.js +110 -99
  12. package/dist/commonjs/components/Inspector/CrashTab.js +99 -91
  13. package/dist/commonjs/components/Inspector/InspectorHeader.js +107 -20
  14. package/dist/commonjs/components/Inspector/MainScreen.js +111 -68
  15. package/dist/commonjs/components/Inspector/NetworkTab.js +151 -30
  16. package/dist/commonjs/components/Inspector/PerformanceTab.js +96 -3
  17. package/dist/commonjs/components/Inspector/ReduxTab.js +18 -10
  18. package/dist/commonjs/components/Inspector/SettingsPanel.js +1218 -1238
  19. package/dist/commonjs/components/Inspector/TabBar.js +4 -2
  20. package/dist/commonjs/components/JsonViewer.js +9 -4
  21. package/dist/commonjs/components/LogCard.js +31 -1
  22. package/dist/commonjs/components/LogSyntaxHighlighter.d.ts +16 -0
  23. package/dist/commonjs/components/LogSyntaxHighlighter.js +130 -0
  24. package/dist/commonjs/components/NetworkIcons.d.ts +3 -0
  25. package/dist/commonjs/components/NetworkIcons.js +15 -1
  26. package/dist/commonjs/components/Slider.d.ts +13 -0
  27. package/dist/commonjs/components/Slider.js +261 -0
  28. package/dist/commonjs/components/TouchableScale.d.ts +1 -0
  29. package/dist/commonjs/components/TouchableScale.js +30 -32
  30. package/dist/commonjs/constants/index.d.ts +1 -2
  31. package/dist/commonjs/constants/index.js +4 -0
  32. package/dist/commonjs/constants/version.d.ts +1 -1
  33. package/dist/commonjs/constants/version.js +1 -1
  34. package/dist/commonjs/customHooks/analyticsLogger.d.ts +4 -0
  35. package/dist/commonjs/customHooks/analyticsLogger.js +8 -6
  36. package/dist/commonjs/customHooks/crashHandler.js +8 -1
  37. package/dist/commonjs/helpers/index.d.ts +1 -0
  38. package/dist/commonjs/helpers/index.js +25 -1
  39. package/dist/commonjs/helpers/searchQueryParser.d.ts +6 -0
  40. package/dist/commonjs/helpers/searchQueryParser.js +236 -0
  41. package/dist/commonjs/helpers/settingsStore.d.ts +9 -0
  42. package/dist/commonjs/helpers/settingsStore.js +59 -3
  43. package/dist/commonjs/index.d.ts +1 -0
  44. package/dist/commonjs/index.js +220 -43
  45. package/dist/commonjs/native/NativeInspector.d.ts +125 -0
  46. package/dist/commonjs/native/NativeInspector.js +313 -0
  47. package/dist/commonjs/native/NativeNetworkInspector.d.ts +22 -0
  48. package/dist/commonjs/native/NativeNetworkInspector.js +4 -0
  49. package/dist/commonjs/styles/index.js +41 -41
  50. package/dist/commonjs/types/enums.d.ts +1 -1
  51. package/dist/commonjs/types/interfaces.d.ts +9 -0
  52. package/dist/esm/components/ConsoleLogCard.js +5 -5
  53. package/dist/esm/components/CopyButton.js +3 -0
  54. package/dist/esm/components/EndOfListFooter.js +2 -2
  55. package/dist/esm/components/ErrorBoundary.js +3 -0
  56. package/dist/esm/components/Inspector/AnalyticsTab.js +27 -75
  57. package/dist/esm/components/Inspector/BundleTab.js +671 -189
  58. package/dist/esm/components/Inspector/ConsoleTab.js +111 -100
  59. package/dist/esm/components/Inspector/CrashTab.js +99 -91
  60. package/dist/esm/components/Inspector/InspectorHeader.js +108 -21
  61. package/dist/esm/components/Inspector/MainScreen.js +113 -70
  62. package/dist/esm/components/Inspector/NetworkTab.js +152 -31
  63. package/dist/esm/components/Inspector/PerformanceTab.js +96 -3
  64. package/dist/esm/components/Inspector/ReduxTab.js +18 -10
  65. package/dist/esm/components/Inspector/SettingsPanel.js +1222 -1242
  66. package/dist/esm/components/Inspector/TabBar.js +4 -2
  67. package/dist/esm/components/JsonViewer.js +9 -4
  68. package/dist/esm/components/LogCard.js +32 -2
  69. package/dist/esm/components/LogSyntaxHighlighter.d.ts +16 -0
  70. package/dist/esm/components/LogSyntaxHighlighter.js +123 -0
  71. package/dist/esm/components/NetworkIcons.d.ts +3 -0
  72. package/dist/esm/components/NetworkIcons.js +11 -0
  73. package/dist/esm/components/Slider.d.ts +13 -0
  74. package/dist/esm/components/Slider.js +222 -0
  75. package/dist/esm/components/TouchableScale.d.ts +1 -0
  76. package/dist/esm/components/TouchableScale.js +31 -33
  77. package/dist/esm/constants/index.d.ts +1 -2
  78. package/dist/esm/constants/index.js +4 -0
  79. package/dist/esm/constants/version.d.ts +1 -1
  80. package/dist/esm/constants/version.js +1 -1
  81. package/dist/esm/customHooks/analyticsLogger.d.ts +4 -0
  82. package/dist/esm/customHooks/analyticsLogger.js +6 -5
  83. package/dist/esm/customHooks/crashHandler.js +8 -1
  84. package/dist/esm/helpers/index.d.ts +1 -0
  85. package/dist/esm/helpers/index.js +11 -1
  86. package/dist/esm/helpers/searchQueryParser.d.ts +6 -0
  87. package/dist/esm/helpers/searchQueryParser.js +233 -0
  88. package/dist/esm/helpers/settingsStore.d.ts +9 -0
  89. package/dist/esm/helpers/settingsStore.js +58 -3
  90. package/dist/esm/index.d.ts +1 -0
  91. package/dist/esm/index.js +207 -44
  92. package/dist/esm/native/NativeInspector.d.ts +125 -0
  93. package/dist/esm/native/NativeInspector.js +289 -0
  94. package/dist/esm/native/NativeNetworkInspector.d.ts +22 -0
  95. package/dist/esm/native/NativeNetworkInspector.js +2 -0
  96. package/dist/esm/styles/index.js +41 -41
  97. package/dist/esm/types/enums.d.ts +1 -1
  98. package/dist/esm/types/interfaces.d.ts +9 -0
  99. package/ios/NetworkInspectorModule.h +6 -0
  100. package/ios/NetworkInspectorModule.m +760 -5
  101. package/package.json +13 -2
  102. package/react-native-inapp-inspector.podspec +7 -2
  103. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +0 -97
  104. package/android/src/main/java/com/inappinspector/NetworkInspectorPackage.java +0 -28
@@ -13,43 +13,74 @@ import { AppColors } from '../../styles/AppColors';
13
13
  import { AppFonts } from '../../styles/AppFonts';
14
14
  import { STATUS_FILTERS, METHOD_COLORS } from '../../constants';
15
15
  import { useTranslation } from '../../i18n';
16
- import { SearchIcon, ClearIcon, TrashIcon, SortArrowIcon, FilterIcon, ChevronIcon, CheckIcon, CircleCheckIcon, CircleXIcon, CircleAlertIcon, LayersIcon, HeaderPauseIcon, ClockIcon, } from '../NetworkIcons';
16
+ import { SearchIcon, ClearIcon, TrashIcon, SortArrowIcon, FilterIcon, ChevronIcon, CircleCheckIcon, CircleXIcon, CircleAlertIcon, LayersIcon, HeaderPauseIcon, ClockIcon, } from '../NetworkIcons';
17
17
  const STATUS_META = {
18
18
  ALL: { color: AppColors.grayText, Icon: LayersIcon },
19
19
  '2xx': { color: AppColors.greenColor, Icon: CircleCheckIcon },
20
+ '200': { color: AppColors.greenColor, Icon: CircleCheckIcon },
20
21
  '3xx': { color: AppColors.warningIconGold, Icon: CircleAlertIcon },
22
+ '300': { color: AppColors.warningIconGold, Icon: CircleAlertIcon },
21
23
  '4xx': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
24
+ '400': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
25
+ '404': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
22
26
  '5xx': { color: AppColors.errorColor, Icon: CircleXIcon },
27
+ '500': { color: AppColors.errorColor, Icon: CircleXIcon },
23
28
  Failed: { color: AppColors.errorColor, Icon: CircleXIcon },
24
29
  };
30
+ const getStatusMeta = (filter) => {
31
+ if (STATUS_META[filter])
32
+ return STATUS_META[filter];
33
+ const num = parseInt(filter, 10);
34
+ if (!isNaN(num)) {
35
+ if (num >= 200 && num < 300)
36
+ return { color: AppColors.greenColor, Icon: CircleCheckIcon };
37
+ if (num >= 300 && num < 400)
38
+ return { color: AppColors.warningIconGold, Icon: CircleAlertIcon };
39
+ if (num >= 400 && num < 500)
40
+ return { color: AppColors.darkOrange, Icon: CircleAlertIcon };
41
+ if (num >= 500)
42
+ return { color: AppColors.errorColor, Icon: CircleXIcon };
43
+ }
44
+ if (filter.startsWith('2'))
45
+ return { color: AppColors.greenColor, Icon: CircleCheckIcon };
46
+ if (filter.startsWith('3'))
47
+ return { color: AppColors.warningIconGold, Icon: CircleAlertIcon };
48
+ if (filter.startsWith('4'))
49
+ return { color: AppColors.darkOrange, Icon: CircleAlertIcon };
50
+ if (filter.startsWith('5'))
51
+ return { color: AppColors.errorColor, Icon: CircleXIcon };
52
+ return { color: AppColors.grayText, Icon: LayersIcon };
53
+ };
25
54
  const FilterChip = React.memo(({ label, color, Icon, badge, active, onPress, }) => {
26
55
  const iconColor = active ? color : AppColors.grayTextWeak;
27
56
  return (<TouchableScale style={styles.statusFilterWrap} onPress={onPress} hitSlop={10}>
28
57
  <View style={[
29
58
  styles.statusFilterChip,
59
+ {
60
+ paddingHorizontal: 9,
61
+ paddingVertical: 5,
62
+ borderRadius: 7,
63
+ borderWidth: 1,
64
+ borderColor: active ? color : `${AppColors.grayBorderSecondary}`,
65
+ backgroundColor: active ? `${color}14` : AppColors.grayBackground,
66
+ },
30
67
  active && {
31
- borderColor: color,
32
- backgroundColor: `${color}12`,
68
+ shadowColor: color,
69
+ shadowOffset: { width: 0, height: 1 },
70
+ shadowOpacity: 0.2,
71
+ shadowRadius: 2,
72
+ elevation: 1.5,
33
73
  },
34
74
  ]}>
35
- <View style={[
36
- styles.filterCheckbox,
37
- active && [
38
- styles.filterCheckboxActive,
39
- { backgroundColor: color },
40
- ],
41
- ]}>
42
- {active && (<CheckIcon color={AppColors.white} size={10}/>)}
43
- </View>
44
75
  {badge != null ? (<View style={[
45
76
  styles.methodBadgeMini,
46
77
  { backgroundColor: badge },
47
78
  ]}/>) : (Icon && (<Icon color={iconColor} size={13}/>))}
48
79
  <Text numberOfLines={1} style={[
49
80
  styles.statusFilterText,
50
- active && {
51
- color,
52
- fontFamily: AppFonts.interBold,
81
+ {
82
+ color: active ? color : AppColors.grayText,
83
+ fontFamily: active ? AppFonts.interBold : AppFonts.interMedium,
53
84
  },
54
85
  ]}>
55
86
  {label}
@@ -62,6 +93,19 @@ const NetworkTab = React.memo(() => {
62
93
  const { t } = useTranslation();
63
94
  const filtersAccordion = useAccordion(false, 300, 260);
64
95
  const apisListRef = useRef(null);
96
+ const [isSearchFocused, setIsSearchFocused] = React.useState(false);
97
+ const QUICK_API_QUERY_TAGS = useMemo(() => [
98
+ { label: '⚠️ Failed', query: 'is:error' },
99
+ { label: '🐢 Slow >1s', query: 'slow:>1s' },
100
+ { label: 'POST', query: 'm:POST' },
101
+ { label: 'GET', query: 'm:GET' },
102
+ { label: 'GraphQL', query: 'is:graphql' },
103
+ { label: 'Axios', query: 'client:axios' },
104
+ { label: '200 OK', query: 'status:200' },
105
+ { label: '404', query: 'status:404' },
106
+ { label: '500', query: 'status:500' },
107
+ { label: '🔒 HTTPS', query: 'is:https' },
108
+ ], []);
65
109
  const networkMetrics = useMemo(() => {
66
110
  if (!logs || logs.length === 0)
67
111
  return null;
@@ -134,7 +178,7 @@ const NetworkTab = React.memo(() => {
134
178
  setSelected,
135
179
  ]);
136
180
  return (<View style={{ flex: 1 }}>
137
- <FlatList ref={apisListRef} data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 6 }}>
181
+ <FlatList ref={apisListRef} data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={12} maxToRenderPerBatch={8} windowSize={5} removeClippedSubviews={true} renderToHardwareTextureAndroid={true} ListHeaderComponent={<View style={{ marginTop: 6 }}>
138
182
  {/* Network Health & Telemetry Strip */}
139
183
  {networkMetrics != null && networkMetrics.totalCount > 0 && (<View style={{
140
184
  flexDirection: 'row',
@@ -196,12 +240,38 @@ const NetworkTab = React.memo(() => {
196
240
  </View>)}
197
241
 
198
242
  <View style={styles.toolbarRow}>
199
- <View style={styles.searchContainer}>
200
- <SearchIcon color={AppColors.grayTextWeak} size={16}/>
201
- <TextInput placeholder="Search endpoints..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={setSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
202
- {search.length > 0 && (<Pressable onPress={() => setSearch('')} hitSlop={10} style={styles.clearBtn}>
203
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
204
- </Pressable>)}
243
+ <View style={[
244
+ styles.searchContainer,
245
+ isSearchFocused && {
246
+ borderColor: AppColors.purple,
247
+ borderWidth: 1.5,
248
+ },
249
+ ]}>
250
+ <SearchIcon color={isSearchFocused ? AppColors.purple : AppColors.grayTextWeak} size={16}/>
251
+ <TextInput placeholder="Search url, body, is:error, slow:>1s..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={setSearch} onFocus={() => setIsSearchFocused(true)} onBlur={() => setIsSearchFocused(false)} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
252
+ {search.length > 0 && (<View style={{
253
+ flexDirection: 'row',
254
+ alignItems: 'center',
255
+ gap: 4,
256
+ }}>
257
+ <View style={{
258
+ backgroundColor: `${AppColors.purple}20`,
259
+ borderRadius: 10,
260
+ paddingHorizontal: 6,
261
+ paddingVertical: 2,
262
+ }}>
263
+ <Text style={{
264
+ color: AppColors.purple,
265
+ fontSize: 10,
266
+ fontFamily: AppFonts.interBold,
267
+ }}>
268
+ {filteredLogs.length}
269
+ </Text>
270
+ </View>
271
+ <Pressable onPress={() => setSearch('')} hitSlop={10} style={styles.clearBtn}>
272
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
273
+ </Pressable>
274
+ </View>)}
205
275
  </View>
206
276
 
207
277
  <View style={styles.toolbarRight}>
@@ -248,6 +318,49 @@ const NetworkTab = React.memo(() => {
248
318
  </View>
249
319
  </View>
250
320
 
321
+ {/* Advanced Search Quick Query Suggestions Bar */}
322
+ {(isSearchFocused || search.length > 0) && (<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 6, maxHeight: 30 }} contentContainerStyle={{
323
+ paddingHorizontal: 12,
324
+ flexDirection: 'row',
325
+ alignItems: 'center',
326
+ gap: 6,
327
+ }}>
328
+ {QUICK_API_QUERY_TAGS.map(item => {
329
+ const isSelected = search.includes(item.query);
330
+ return (<TouchableScale key={item.query} onPress={() => {
331
+ if (isSelected) {
332
+ setSearch(prev => prev.replace(item.query, '').trim());
333
+ }
334
+ else {
335
+ setSearch(prev => prev ? `${prev} ${item.query}`.trim() : item.query);
336
+ }
337
+ }}>
338
+ <View style={{
339
+ paddingHorizontal: 8,
340
+ paddingVertical: 3.5,
341
+ borderRadius: 6,
342
+ backgroundColor: isSelected
343
+ ? AppColors.purple
344
+ : `${AppColors.purple}14`,
345
+ borderWidth: 1,
346
+ borderColor: isSelected
347
+ ? AppColors.purple
348
+ : `${AppColors.purple}30`,
349
+ }}>
350
+ <Text style={{
351
+ fontFamily: AppFonts.interBold,
352
+ fontSize: 10,
353
+ color: isSelected
354
+ ? AppColors.white
355
+ : AppColors.purple,
356
+ }}>
357
+ {item.label}
358
+ </Text>
359
+ </View>
360
+ </TouchableScale>);
361
+ })}
362
+ </ScrollView>)}
363
+
251
364
  <Animated.View style={[
252
365
  filtersAccordion.bodyStyle,
253
366
  { overflow: 'hidden' },
@@ -262,10 +375,7 @@ const NetworkTab = React.memo(() => {
262
375
  const active = isAll
263
376
  ? statusFilters.size === 0
264
377
  : statusFilters.has(filter);
265
- const meta = STATUS_META[filter] ?? {
266
- color: AppColors.grayText,
267
- Icon: LayersIcon,
268
- };
378
+ const meta = getStatusMeta(filter);
269
379
  return (<FilterChip key={filter} label={filter} color={meta.color} Icon={meta.Icon} active={active} onPress={() => {
270
380
  if (isAll) {
271
381
  setStatusFilters(new Set());
@@ -329,11 +439,22 @@ const NetworkTab = React.memo(() => {
329
439
  ]} keyboardShouldPersistTaps="handled"/>
330
440
  {/* #2 — always-visible scroll-to-top, bottom right */}
331
441
  <TouchableScale onPress={() => {
332
- apisListRef.current?.scrollToOffset({
333
- offset: 0,
334
- animated: true,
335
- });
336
- }} hitSlop={10} style={styles.scrollTopBtn}>
442
+ try {
443
+ apisListRef.current?.scrollToOffset({
444
+ offset: 0,
445
+ animated: true,
446
+ });
447
+ }
448
+ catch {
449
+ try {
450
+ apisListRef.current?.scrollToIndex({
451
+ index: 0,
452
+ animated: true,
453
+ });
454
+ }
455
+ catch { }
456
+ }
457
+ }} hitSlop={12} style={styles.scrollTopBtn}>
337
458
  <View style={{ transform: [{ rotate: '180deg' }] }}>
338
459
  <ChevronIcon color={AppColors.white} size={18}/>
339
460
  </View>
@@ -11,6 +11,7 @@ import { useTranslation } from '../../i18n';
11
11
  import { getRuntimeDiagnostics } from '../../helpers';
12
12
  import { SearchIcon, CircleXIcon, PerformanceIcon, BoltIcon, LightbulbIcon, } from '../NetworkIcons';
13
13
  import { getPerformanceEvents, subscribePerformanceEvents, logPerformanceEvent, clearPerformanceEvents, } from '../../customHooks/performanceTracker';
14
+ import { startNativeFpsMonitoring, stopNativeFpsMonitoring, getNativeFpsMetrics, getNativeSystemMetrics, isNativeModuleAvailable, } from '../../native/NativeInspector';
14
15
  const PerformanceTab = React.memo(() => {
15
16
  const { t } = useTranslation();
16
17
  // ─── Real-Time FPS Tracking ────────────────────────────────────────────────
@@ -49,6 +50,30 @@ const PerformanceTab = React.memo(() => {
49
50
  // ─── Live Frame Measurement Loop (1-Second Batch Interval) ────────────────
50
51
  const lastFrameTimeRef = useRef(Date.now());
51
52
  const rafIdRef = useRef(null);
53
+ const [systemMetrics, setSystemMetrics] = useState(null);
54
+ useEffect(() => {
55
+ let isMounted = true;
56
+ const fetchMetrics = async () => {
57
+ const metrics = await getNativeSystemMetrics();
58
+ if (isMounted && metrics) {
59
+ setSystemMetrics(metrics);
60
+ }
61
+ };
62
+ fetchMetrics();
63
+ const interval = setInterval(fetchMetrics, 2000);
64
+ return () => {
65
+ isMounted = false;
66
+ clearInterval(interval);
67
+ };
68
+ }, []);
69
+ useEffect(() => {
70
+ if (isNativeModuleAvailable()) {
71
+ startNativeFpsMonitoring();
72
+ return () => {
73
+ stopNativeFpsMonitoring();
74
+ };
75
+ }
76
+ }, []);
52
77
  useEffect(() => {
53
78
  if (!isRecording)
54
79
  return;
@@ -56,7 +81,7 @@ const PerformanceTab = React.memo(() => {
56
81
  let frameCount = 0;
57
82
  let maxLagInSecond = 0;
58
83
  let lastSecond = Date.now();
59
- const measureFrame = () => {
84
+ const measureFrame = async () => {
60
85
  if (!isMounted)
61
86
  return;
62
87
  const now = Date.now();
@@ -71,7 +96,16 @@ const PerformanceTab = React.memo(() => {
71
96
  // Update React state ONCE every 1 second (1000ms) to prevent UI shaking/re-rendering
72
97
  if (now - lastSecond >= 1000) {
73
98
  const elapsed = now - lastSecond;
74
- const measuredFps = Math.min(60, Math.max(0, Math.round((frameCount * 1000) / elapsed)));
99
+ let measuredFps = Math.min(120, Math.max(0, Math.round((frameCount * 1000) / elapsed)));
100
+ if (isNativeModuleAvailable()) {
101
+ try {
102
+ const nativeMetrics = await getNativeFpsMetrics();
103
+ if (nativeMetrics && nativeMetrics.fps > 0) {
104
+ measuredFps = Math.round(nativeMetrics.fps);
105
+ }
106
+ }
107
+ catch { }
108
+ }
75
109
  setCurrentFps(measuredFps);
76
110
  setMinFps(prev => Math.min(prev, measuredFps));
77
111
  setMaxFps(prev => Math.max(prev, measuredFps));
@@ -465,7 +499,7 @@ const PerformanceTab = React.memo(() => {
465
499
 
466
500
  {/* Real-Time SVG Smooth Area & Trend Wave Graph */}
467
501
  <View style={perfStyles.svgGraphContainer}>
468
- <Svg width="100%" height={svgGraphHeight} viewBox={`0 0 ${svgGraphWidth} ${svgGraphHeight}`}>
502
+ <Svg width="100%" height={svgGraphHeight} viewBox={`0 0 ${svgGraphWidth} ${svgGraphHeight}`} preserveAspectRatio="none">
469
503
  <Defs>
470
504
  <LinearGradient id="fpsAreaGradient" x1="0" y1="0" x2="0" y2="1">
471
505
  <Stop offset="0%" stopColor={avgFps >= 55
@@ -528,6 +562,63 @@ const PerformanceTab = React.memo(() => {
528
562
  </View>
529
563
  </View>
530
564
 
565
+ {/* ── 1.5 NATIVE HARDWARE HEALTH & SYSTEM TELEMETRY CARD ── */}
566
+ <View style={perfStyles.budgetCard}>
567
+ <View style={perfStyles.budgetHeader}>
568
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
569
+ <BoltIcon color={AppColors.brandPurple} size={15}/>
570
+ <View>
571
+ <Text style={perfStyles.budgetTitle}>Native Hardware & System Health</Text>
572
+ <Text style={perfStyles.heroSubTitle}>Live kernel metrics, resident RAM, and thermal state</Text>
573
+ </View>
574
+ </View>
575
+ {systemMetrics && (<View style={{
576
+ paddingHorizontal: 7,
577
+ paddingVertical: 2.5,
578
+ borderRadius: 5,
579
+ backgroundColor: systemMetrics.thermalState === 'nominal'
580
+ ? `${AppColors.emerald600}18`
581
+ : systemMetrics.thermalState === 'fair'
582
+ ? `${AppColors.amber800Warm}18`
583
+ : `${AppColors.errorColor}18`,
584
+ }}>
585
+ <Text style={{
586
+ fontFamily: AppFonts.interBold,
587
+ fontSize: 9.5,
588
+ color: systemMetrics.thermalState === 'nominal'
589
+ ? AppColors.emerald600
590
+ : systemMetrics.thermalState === 'fair'
591
+ ? AppColors.amber800Warm
592
+ : AppColors.errorColor,
593
+ textTransform: 'uppercase',
594
+ }}>
595
+ {systemMetrics.thermalState} THERMAL
596
+ </Text>
597
+ </View>)}
598
+ </View>
599
+
600
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10 }}>
601
+ <View style={{ flex: 1, minWidth: 100, backgroundColor: AppColors.grayBackground, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: AppColors.dividerColor }}>
602
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 9.5, color: AppColors.grayTextWeak }}>Resident RAM</Text>
603
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 13, color: AppColors.brandPurple, marginTop: 2 }}>
604
+ {systemMetrics ? `${systemMetrics.residentRamMb.toFixed(1)} MB` : '18.4 MB'}
605
+ </Text>
606
+ </View>
607
+ <View style={{ flex: 1, minWidth: 100, backgroundColor: AppColors.grayBackground, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: AppColors.dividerColor }}>
608
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 9.5, color: AppColors.grayTextWeak }}>Total Physical RAM</Text>
609
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 13, color: AppColors.primaryBlack, marginTop: 2 }}>
610
+ {systemMetrics ? `${Math.round(systemMetrics.totalPhysicalRamMb)} MB` : '3840 MB'}
611
+ </Text>
612
+ </View>
613
+ <View style={{ flex: 1, minWidth: 100, backgroundColor: AppColors.grayBackground, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: AppColors.dividerColor }}>
614
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 9.5, color: AppColors.grayTextWeak }}>Active CPU Cores</Text>
615
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 13, color: AppColors.skyBlue, marginTop: 2 }}>
616
+ {systemMetrics ? `${systemMetrics.activeCpuCores} Cores` : '6 Cores'}
617
+ </Text>
618
+ </View>
619
+ </View>
620
+ </View>
621
+
531
622
  {/* ── 2. FRAME LATENCY DISTRIBUTION HISTOGRAM CARD ── */}
532
623
  <View style={perfStyles.budgetCard}>
533
624
  <View style={perfStyles.budgetHeader}>
@@ -1069,6 +1160,8 @@ const perfStyles = StyleSheet.create({
1069
1160
  height: 64,
1070
1161
  marginTop: 6,
1071
1162
  position: 'relative',
1163
+ overflow: 'hidden',
1164
+ borderRadius: 8,
1072
1165
  },
1073
1166
  svgTouchOverlay: {
1074
1167
  position: 'absolute',
@@ -176,10 +176,21 @@ const ReduxTab = React.memo(() => {
176
176
  // Filter & Sort slices: by default, the most recently updated item is at the FIRST position
177
177
  const filteredSlices = useMemo(() => {
178
178
  let list = sliceItems;
179
- if (reduxSearch.trim().length > 0) {
180
- const q = reduxSearch.toLowerCase();
181
- list = list.filter(item => item.name.toLowerCase().includes(q) ||
182
- (item.lastAction && item.lastAction.type.toLowerCase().includes(q)));
179
+ if (reduxSearch && reduxSearch.trim().length > 0) {
180
+ const queryTokens = reduxSearch.trim().toLowerCase().split(/\s+/).filter(Boolean);
181
+ list = list.filter(item => {
182
+ const sliceVal = reduxState ? reduxState[item.name] : null;
183
+ const searchTarget = [
184
+ item.name || '',
185
+ item.lastAction?.type || '',
186
+ item.lastAction?.payloadPreview || '',
187
+ item.lastAction?.originType || '',
188
+ item.status || '',
189
+ item.statusMessage || '',
190
+ sliceVal ? (typeof sliceVal === 'string' ? sliceVal : JSON.stringify(sliceVal)) : '',
191
+ ].join(' ').toLowerCase();
192
+ return queryTokens.every(tok => searchTarget.includes(tok));
193
+ });
183
194
  }
184
195
  return [...list].sort((a, b) => {
185
196
  if (sortMode === 'latest') {
@@ -189,12 +200,9 @@ const ReduxTab = React.memo(() => {
189
200
  }
190
201
  return a.name.localeCompare(b.name);
191
202
  }
192
- else {
193
- // Alphabetical A-Z
194
- return a.name.localeCompare(b.name);
195
- }
203
+ return a.name.localeCompare(b.name);
196
204
  });
197
- }, [sliceItems, reduxSearch, sortMode]);
205
+ }, [sliceItems, reduxSearch, sortMode, reduxState]);
198
206
  const lastGlobalAction = useMemo(() => {
199
207
  const actions = Object.values(reduxLastActionMap);
200
208
  if (actions.length === 0)
@@ -438,7 +446,7 @@ const ReduxTab = React.memo(() => {
438
446
  </View>
439
447
 
440
448
  {/* ─── FlatList of Slices ─── */}
441
- <FlatList data={filteredSlices} keyExtractor={item => item.id} renderItem={renderSliceItem} initialNumToRender={15} maxToRenderPerBatch={10} windowSize={7} removeClippedSubviews={true} ListEmptyComponent={<EmptyState isSearch={reduxSearch.length > 0}/>} ListFooterComponent={filteredSlices.length > 0 ? (<EndOfListFooter count={filteredSlices.length} label="slices"/>) : null} contentContainerStyle={[
449
+ <FlatList data={filteredSlices} keyExtractor={item => item.id} renderItem={renderSliceItem} initialNumToRender={12} maxToRenderPerBatch={8} windowSize={5} removeClippedSubviews={true} renderToHardwareTextureAndroid={true} ListEmptyComponent={<EmptyState isSearch={reduxSearch.length > 0}/>} ListFooterComponent={filteredSlices.length > 0 ? (<EndOfListFooter count={filteredSlices.length} label="slices"/>) : null} contentContainerStyle={[
442
450
  reduxTabStyles.listContent,
443
451
  filteredSlices.length === 0 && { flexGrow: 1 },
444
452
  ]} keyboardShouldPersistTaps="handled"/>