react-native-inapp-inspector 1.1.34 → 2.0.0

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 (122) 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 +904 -0
  4. package/android/src/main/java/com/inappinspector/NetworkInspectorPackage.kt +17 -0
  5. package/dist/commonjs/components/ConsoleLogCard.js +57 -56
  6. package/dist/commonjs/components/CopyButton.js +10 -1
  7. package/dist/commonjs/components/ErrorBoundary.js +3 -0
  8. package/dist/commonjs/components/Inspector/AnalyticsTab.js +7 -3
  9. package/dist/commonjs/components/Inspector/BundleTab.js +20 -20
  10. package/dist/commonjs/components/Inspector/ConsoleTab.js +1 -1
  11. package/dist/commonjs/components/Inspector/CrashDetail.js +16 -12
  12. package/dist/commonjs/components/Inspector/CrashTab.js +97 -90
  13. package/dist/commonjs/components/Inspector/InspectorHeader.js +28 -24
  14. package/dist/commonjs/components/Inspector/MainScreen.js +20 -103
  15. package/dist/commonjs/components/Inspector/NetworkTab.js +143 -10
  16. package/dist/commonjs/components/Inspector/PerformanceTab.js +96 -3
  17. package/dist/commonjs/components/Inspector/ReduxDetail.js +24 -19
  18. package/dist/commonjs/components/Inspector/ReduxTab.js +30 -18
  19. package/dist/commonjs/components/Inspector/SettingsPanel.js +1578 -754
  20. package/dist/commonjs/components/Inspector/TabBar.js +6 -3
  21. package/dist/commonjs/components/LogCard.js +27 -9
  22. package/dist/commonjs/components/MetaAccordion.js +42 -0
  23. package/dist/commonjs/components/NetworkIcons.d.ts +10 -0
  24. package/dist/commonjs/components/NetworkIcons.js +53 -1
  25. package/dist/commonjs/components/Toast.js +3 -3
  26. package/dist/commonjs/components/TouchableScale.d.ts +5 -2
  27. package/dist/commonjs/components/TouchableScale.js +30 -32
  28. package/dist/commonjs/constants/index.d.ts +1 -2
  29. package/dist/commonjs/constants/index.js +4 -0
  30. package/dist/commonjs/constants/version.d.ts +1 -1
  31. package/dist/commonjs/constants/version.js +1 -1
  32. package/dist/commonjs/customHooks/analyticsLogger.d.ts +6 -0
  33. package/dist/commonjs/customHooks/analyticsLogger.js +16 -5
  34. package/dist/commonjs/customHooks/bundleAnalyzer.d.ts +2 -0
  35. package/dist/commonjs/customHooks/bundleAnalyzer.js +26 -17
  36. package/dist/commonjs/customHooks/consoleLogger.d.ts +2 -0
  37. package/dist/commonjs/customHooks/consoleLogger.js +10 -1
  38. package/dist/commonjs/customHooks/crashHandler.d.ts +2 -0
  39. package/dist/commonjs/customHooks/crashHandler.js +17 -1
  40. package/dist/commonjs/customHooks/networkLogger.d.ts +2 -0
  41. package/dist/commonjs/customHooks/networkLogger.js +12 -1
  42. package/dist/commonjs/customHooks/performanceTracker.d.ts +3 -1
  43. package/dist/commonjs/customHooks/performanceTracker.js +18 -4
  44. package/dist/commonjs/customHooks/reduxLogger.d.ts +2 -0
  45. package/dist/commonjs/customHooks/reduxLogger.js +10 -1
  46. package/dist/commonjs/helpers/index.js +10 -1
  47. package/dist/commonjs/helpers/settingsStore.d.ts +9 -0
  48. package/dist/commonjs/helpers/settingsStore.js +59 -3
  49. package/dist/commonjs/i18n/locales/en.json +96 -28
  50. package/dist/commonjs/index.d.ts +1 -0
  51. package/dist/commonjs/index.js +307 -73
  52. package/dist/commonjs/native/NativeInspector.d.ts +125 -0
  53. package/dist/commonjs/native/NativeInspector.js +313 -0
  54. package/dist/commonjs/native/NativeNetworkInspector.d.ts +22 -0
  55. package/dist/commonjs/native/NativeNetworkInspector.js +4 -0
  56. package/dist/commonjs/styles/AppColors.d.ts +28 -0
  57. package/dist/commonjs/styles/AppColors.js +30 -0
  58. package/dist/commonjs/styles/index.js +6 -6
  59. package/dist/commonjs/types/enums.d.ts +1 -1
  60. package/dist/commonjs/types/interfaces.d.ts +9 -0
  61. package/dist/esm/components/ConsoleLogCard.js +58 -57
  62. package/dist/esm/components/CopyButton.js +10 -1
  63. package/dist/esm/components/ErrorBoundary.js +3 -0
  64. package/dist/esm/components/Inspector/AnalyticsTab.js +7 -3
  65. package/dist/esm/components/Inspector/BundleTab.js +20 -20
  66. package/dist/esm/components/Inspector/ConsoleTab.js +1 -1
  67. package/dist/esm/components/Inspector/CrashDetail.js +16 -12
  68. package/dist/esm/components/Inspector/CrashTab.js +97 -90
  69. package/dist/esm/components/Inspector/InspectorHeader.js +29 -25
  70. package/dist/esm/components/Inspector/MainScreen.js +21 -71
  71. package/dist/esm/components/Inspector/NetworkTab.js +145 -12
  72. package/dist/esm/components/Inspector/PerformanceTab.js +96 -3
  73. package/dist/esm/components/Inspector/ReduxDetail.js +24 -19
  74. package/dist/esm/components/Inspector/ReduxTab.js +30 -18
  75. package/dist/esm/components/Inspector/SettingsPanel.js +1582 -758
  76. package/dist/esm/components/Inspector/TabBar.js +7 -4
  77. package/dist/esm/components/LogCard.js +27 -9
  78. package/dist/esm/components/MetaAccordion.js +42 -0
  79. package/dist/esm/components/NetworkIcons.d.ts +10 -0
  80. package/dist/esm/components/NetworkIcons.js +42 -0
  81. package/dist/esm/components/Toast.js +3 -3
  82. package/dist/esm/components/TouchableScale.d.ts +5 -2
  83. package/dist/esm/components/TouchableScale.js +31 -33
  84. package/dist/esm/constants/index.d.ts +1 -2
  85. package/dist/esm/constants/index.js +4 -0
  86. package/dist/esm/constants/version.d.ts +1 -1
  87. package/dist/esm/constants/version.js +1 -1
  88. package/dist/esm/customHooks/analyticsLogger.d.ts +6 -0
  89. package/dist/esm/customHooks/analyticsLogger.js +12 -4
  90. package/dist/esm/customHooks/bundleAnalyzer.d.ts +2 -0
  91. package/dist/esm/customHooks/bundleAnalyzer.js +23 -16
  92. package/dist/esm/customHooks/consoleLogger.d.ts +2 -0
  93. package/dist/esm/customHooks/consoleLogger.js +7 -0
  94. package/dist/esm/customHooks/crashHandler.d.ts +2 -0
  95. package/dist/esm/customHooks/crashHandler.js +14 -0
  96. package/dist/esm/customHooks/networkLogger.d.ts +2 -0
  97. package/dist/esm/customHooks/networkLogger.js +9 -0
  98. package/dist/esm/customHooks/performanceTracker.d.ts +3 -1
  99. package/dist/esm/customHooks/performanceTracker.js +15 -3
  100. package/dist/esm/customHooks/reduxLogger.d.ts +2 -0
  101. package/dist/esm/customHooks/reduxLogger.js +7 -0
  102. package/dist/esm/helpers/index.js +10 -1
  103. package/dist/esm/helpers/settingsStore.d.ts +9 -0
  104. package/dist/esm/helpers/settingsStore.js +58 -3
  105. package/dist/esm/i18n/locales/en.json +96 -28
  106. package/dist/esm/index.d.ts +1 -0
  107. package/dist/esm/index.js +279 -59
  108. package/dist/esm/native/NativeInspector.d.ts +125 -0
  109. package/dist/esm/native/NativeInspector.js +289 -0
  110. package/dist/esm/native/NativeNetworkInspector.d.ts +22 -0
  111. package/dist/esm/native/NativeNetworkInspector.js +2 -0
  112. package/dist/esm/styles/AppColors.d.ts +28 -0
  113. package/dist/esm/styles/AppColors.js +30 -0
  114. package/dist/esm/styles/index.js +6 -6
  115. package/dist/esm/types/enums.d.ts +1 -1
  116. package/dist/esm/types/interfaces.d.ts +9 -0
  117. package/ios/NetworkInspectorModule.h +6 -0
  118. package/ios/NetworkInspectorModule.m +720 -5
  119. package/package.json +15 -3
  120. package/react-native-inapp-inspector.podspec +7 -2
  121. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +0 -97
  122. package/android/src/main/java/com/inappinspector/NetworkInspectorPackage.java +0 -28
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useRef } from 'react';
1
+ import React, { useCallback, useMemo, useRef } from 'react';
2
2
  import { Animated, FlatList, Pressable, ScrollView, Text, TextInput, View, } from 'react-native';
3
3
  import { animateNextLayout, useInspector } from './InspectorContext';
4
4
  import useAccordion from '../../customHooks/useAccordion';
@@ -12,15 +12,45 @@ import styles from '../../styles';
12
12
  import { AppColors } from '../../styles/AppColors';
13
13
  import { AppFonts } from '../../styles/AppFonts';
14
14
  import { STATUS_FILTERS, METHOD_COLORS } from '../../constants';
15
- import { SearchIcon, ClearIcon, TrashIcon, SortArrowIcon, FilterIcon, ChevronIcon, CheckIcon, CircleCheckIcon, CircleXIcon, CircleAlertIcon, LayersIcon, HeaderPauseIcon, } from '../NetworkIcons';
15
+ import { useTranslation } from '../../i18n';
16
+ import { SearchIcon, ClearIcon, TrashIcon, SortArrowIcon, FilterIcon, ChevronIcon, CheckIcon, CircleCheckIcon, CircleXIcon, CircleAlertIcon, LayersIcon, HeaderPauseIcon, ClockIcon, } from '../NetworkIcons';
16
17
  const STATUS_META = {
17
18
  ALL: { color: AppColors.grayText, Icon: LayersIcon },
18
19
  '2xx': { color: AppColors.greenColor, Icon: CircleCheckIcon },
20
+ '200': { color: AppColors.greenColor, Icon: CircleCheckIcon },
19
21
  '3xx': { color: AppColors.warningIconGold, Icon: CircleAlertIcon },
22
+ '300': { color: AppColors.warningIconGold, Icon: CircleAlertIcon },
20
23
  '4xx': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
24
+ '400': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
25
+ '404': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
21
26
  '5xx': { color: AppColors.errorColor, Icon: CircleXIcon },
27
+ '500': { color: AppColors.errorColor, Icon: CircleXIcon },
22
28
  Failed: { color: AppColors.errorColor, Icon: CircleXIcon },
23
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
+ };
24
54
  const FilterChip = React.memo(({ label, color, Icon, badge, active, onPress, }) => {
25
55
  const iconColor = active ? color : AppColors.grayTextWeak;
26
56
  return (<TouchableScale style={styles.statusFilterWrap} onPress={onPress} hitSlop={10}>
@@ -58,8 +88,43 @@ const FilterChip = React.memo(({ label, color, Icon, badge, active, onPress, })
58
88
  });
59
89
  const NetworkTab = React.memo(() => {
60
90
  const { groupedData, search, setSearch, handleDelete, selectedLogs, sortOrder, setSortOrder, statusFilters, setStatusFilters, methodFilters, setMethodFilters, availableMethods, filteredLogs, logs, toggleSectionFilter, toggleSectionCollapse, minStart, totalRange, newLogIds, toggleSelect, setSelected, isNetworkPaused, setIsNetworkPaused, } = useInspector();
91
+ const { t } = useTranslation();
61
92
  const filtersAccordion = useAccordion(false, 300, 260);
62
93
  const apisListRef = useRef(null);
94
+ const networkMetrics = useMemo(() => {
95
+ if (!logs || logs.length === 0)
96
+ return null;
97
+ let successCount = 0;
98
+ let completedCount = 0;
99
+ let totalDuration = 0;
100
+ let totalBytes = 0;
101
+ const durations = [];
102
+ logs.forEach(log => {
103
+ if (log.status != null) {
104
+ completedCount++;
105
+ const s = typeof log.status === 'number' ? log.status : parseInt(String(log.status), 10);
106
+ if (!isNaN(s)) {
107
+ if (s >= 200 && s < 400) {
108
+ successCount++;
109
+ }
110
+ }
111
+ }
112
+ if (typeof log.duration === 'number' && log.duration > 0) {
113
+ durations.push(log.duration);
114
+ totalDuration += log.duration;
115
+ }
116
+ });
117
+ durations.sort((a, b) => a - b);
118
+ const avgDuration = durations.length > 0 ? Math.round(totalDuration / durations.length) : 0;
119
+ const p95 = durations.length > 0 ? durations[Math.floor(durations.length * 0.95)] : 0;
120
+ const successRate = completedCount > 0 ? Math.round((successCount / completedCount) * 100) : 100;
121
+ return {
122
+ successRate,
123
+ avgDuration,
124
+ p95,
125
+ totalCount: logs.length,
126
+ };
127
+ }, [logs]);
63
128
  const renderItem = useCallback(({ item, index }) => {
64
129
  if (item.type === 'header') {
65
130
  return (<AnimatedEntrance index={index} distance={8}>
@@ -98,7 +163,67 @@ const NetworkTab = React.memo(() => {
98
163
  setSelected,
99
164
  ]);
100
165
  return (<View style={{ flex: 1 }}>
101
- <FlatList ref={apisListRef} data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 8 }}>
166
+ <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 }}>
167
+ {/* Network Health & Telemetry Strip */}
168
+ {networkMetrics != null && networkMetrics.totalCount > 0 && (<View style={{
169
+ flexDirection: 'row',
170
+ alignItems: 'center',
171
+ justifyContent: 'space-between',
172
+ backgroundColor: AppColors.grayBackground,
173
+ borderRadius: 8,
174
+ paddingHorizontal: 10,
175
+ paddingVertical: 5.5,
176
+ marginHorizontal: 12,
177
+ marginBottom: 6,
178
+ borderWidth: 1,
179
+ borderColor: AppColors.dividerColor,
180
+ }}>
181
+ {/* Success Rate */}
182
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4.5 }}>
183
+ <View style={{
184
+ width: 6,
185
+ height: 6,
186
+ borderRadius: 3,
187
+ backgroundColor: networkMetrics.successRate >= 95
188
+ ? AppColors.greenColor
189
+ : networkMetrics.successRate >= 80
190
+ ? AppColors.amber500
191
+ : AppColors.errorColor,
192
+ }}/>
193
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 10.5, color: AppColors.primaryBlack }}>
194
+ {networkMetrics.successRate}%
195
+ </Text>
196
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 10, color: AppColors.grayTextWeak }}>
197
+ {t('network.successRate')}
198
+ </Text>
199
+ </View>
200
+
201
+ <View style={{ width: 1, height: 11, backgroundColor: AppColors.dividerColor }}/>
202
+
203
+ {/* Avg Latency */}
204
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
205
+ <ClockIcon color={AppColors.purple} size={10}/>
206
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 10.5, color: AppColors.purple }}>
207
+ {networkMetrics.avgDuration}ms
208
+ </Text>
209
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 10, color: AppColors.grayTextWeak }}>
210
+ {t('network.avgLatency')}
211
+ </Text>
212
+ </View>
213
+
214
+ <View style={{ width: 1, height: 11, backgroundColor: AppColors.dividerColor }}/>
215
+
216
+ {/* P95 Latency */}
217
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
218
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 10.5, color: AppColors.primaryBlack }}>
219
+ {networkMetrics.p95}ms
220
+ </Text>
221
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 10, color: AppColors.grayTextWeak }}>
222
+ P95
223
+ </Text>
224
+ </View>
225
+ </View>)}
226
+
102
227
  <View style={styles.toolbarRow}>
103
228
  <View style={styles.searchContainer}>
104
229
  <SearchIcon color={AppColors.grayTextWeak} size={16}/>
@@ -166,10 +291,7 @@ const NetworkTab = React.memo(() => {
166
291
  const active = isAll
167
292
  ? statusFilters.size === 0
168
293
  : statusFilters.has(filter);
169
- const meta = STATUS_META[filter] ?? {
170
- color: AppColors.grayText,
171
- Icon: LayersIcon,
172
- };
294
+ const meta = getStatusMeta(filter);
173
295
  return (<FilterChip key={filter} label={filter} color={meta.color} Icon={meta.Icon} active={active} onPress={() => {
174
296
  if (isAll) {
175
297
  setStatusFilters(new Set());
@@ -233,11 +355,22 @@ const NetworkTab = React.memo(() => {
233
355
  ]} keyboardShouldPersistTaps="handled"/>
234
356
  {/* #2 — always-visible scroll-to-top, bottom right */}
235
357
  <TouchableScale onPress={() => {
236
- apisListRef.current?.scrollToOffset({
237
- offset: 0,
238
- animated: true,
239
- });
240
- }} hitSlop={10} style={styles.scrollTopBtn}>
358
+ try {
359
+ apisListRef.current?.scrollToOffset({
360
+ offset: 0,
361
+ animated: true,
362
+ });
363
+ }
364
+ catch {
365
+ try {
366
+ apisListRef.current?.scrollToIndex({
367
+ index: 0,
368
+ animated: true,
369
+ });
370
+ }
371
+ catch { }
372
+ }
373
+ }} hitSlop={12} style={styles.scrollTopBtn}>
241
374
  <View style={{ transform: [{ rotate: '180deg' }] }}>
242
375
  <ChevronIcon color={AppColors.white} size={18}/>
243
376
  </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',
@@ -976,30 +976,30 @@ const reduxDetailStyles = StyleSheet.create({
976
976
  flexDirection: 'row',
977
977
  alignItems: 'center',
978
978
  gap: 3.5,
979
- backgroundColor: '#ECFDF5',
979
+ backgroundColor: AppColors.mintGreenBg,
980
980
  paddingHorizontal: 6,
981
981
  paddingVertical: 2,
982
982
  borderRadius: 4,
983
983
  borderWidth: 1,
984
- borderColor: '#A7F3D0',
984
+ borderColor: AppColors.mintGreenBorder,
985
985
  },
986
986
  persistedChipText: {
987
987
  fontFamily: AppFonts.interBold,
988
988
  fontSize: 9,
989
- color: '#047857',
989
+ color: AppColors.mintGreenText,
990
990
  },
991
991
  inMemoryChip: {
992
- backgroundColor: '#EEF2FF',
992
+ backgroundColor: AppColors.indigo50,
993
993
  paddingHorizontal: 6,
994
994
  paddingVertical: 2,
995
995
  borderRadius: 4,
996
996
  borderWidth: 1,
997
- borderColor: '#C7D2FE',
997
+ borderColor: AppColors.blueBorder,
998
998
  },
999
999
  inMemoryChipText: {
1000
1000
  fontFamily: AppFonts.interBold,
1001
1001
  fontSize: 9,
1002
- color: '#4338CA',
1002
+ color: AppColors.indigo600Alt,
1003
1003
  },
1004
1004
  subStatsRow: {
1005
1005
  flexDirection: 'row',
@@ -1012,12 +1012,17 @@ const reduxDetailStyles = StyleSheet.create({
1012
1012
  alignItems: 'center',
1013
1013
  gap: 4,
1014
1014
  backgroundColor: AppColors.grayBackground,
1015
- paddingHorizontal: 7,
1016
- paddingVertical: 3.5,
1017
- borderRadius: 6,
1015
+ paddingHorizontal: 6,
1016
+ paddingVertical: 2.5,
1017
+ borderRadius: 5,
1018
1018
  borderWidth: 1,
1019
1019
  borderColor: AppColors.dividerColor,
1020
1020
  },
1021
+ statPillText: {
1022
+ fontFamily: AppFonts.interMedium,
1023
+ fontSize: 10,
1024
+ color: AppColors.grayText,
1025
+ },
1021
1026
  statLabel: {
1022
1027
  fontFamily: AppFonts.interBold,
1023
1028
  fontSize: 10,
@@ -1067,17 +1072,17 @@ const reduxDetailStyles = StyleSheet.create({
1067
1072
  gap: 8,
1068
1073
  },
1069
1074
  actionBadge: {
1070
- backgroundColor: '#EEF2FF',
1075
+ backgroundColor: AppColors.indigo50,
1071
1076
  paddingHorizontal: 6,
1072
1077
  paddingVertical: 2,
1073
1078
  borderRadius: 4,
1074
1079
  borderWidth: 1,
1075
- borderColor: '#C7D2FE',
1080
+ borderColor: AppColors.blueBorder,
1076
1081
  },
1077
1082
  actionBadgeText: {
1078
1083
  fontFamily: AppFonts.interBold,
1079
1084
  fontSize: 9.5,
1080
- color: '#4338CA',
1085
+ color: AppColors.indigo600Alt,
1081
1086
  },
1082
1087
  actionTitle: {
1083
1088
  flex: 1,
@@ -1158,22 +1163,22 @@ const reduxDetailStyles = StyleSheet.create({
1158
1163
  textAlign: 'right',
1159
1164
  },
1160
1165
  persistedInfoCard: {
1161
- backgroundColor: '#ECFDF5',
1166
+ backgroundColor: AppColors.mintGreenBg,
1162
1167
  borderRadius: 8,
1163
1168
  borderWidth: 1,
1164
- borderColor: '#A7F3D0',
1169
+ borderColor: AppColors.mintGreenBorder,
1165
1170
  padding: 10,
1166
1171
  },
1167
1172
  persistedInfoTitle: {
1168
1173
  fontFamily: AppFonts.interBold,
1169
1174
  fontSize: 11,
1170
- color: '#047857',
1175
+ color: AppColors.mintGreenText,
1171
1176
  },
1172
1177
  clearPersistMiniBtn: {
1173
1178
  flexDirection: 'row',
1174
1179
  alignItems: 'center',
1175
1180
  gap: 3,
1176
- backgroundColor: '#FEE2E2',
1181
+ backgroundColor: AppColors.errorCardBg,
1177
1182
  paddingHorizontal: 6,
1178
1183
  paddingVertical: 2.5,
1179
1184
  borderRadius: 4,
@@ -1190,7 +1195,7 @@ const reduxDetailStyles = StyleSheet.create({
1190
1195
  gap: 6,
1191
1196
  backgroundColor: AppColors.errorCardBg,
1192
1197
  borderWidth: 1,
1193
- borderColor: '#FEE2E2',
1198
+ borderColor: AppColors.errorBorder,
1194
1199
  borderRadius: 8,
1195
1200
  paddingVertical: 10,
1196
1201
  marginTop: 14,
@@ -1224,8 +1229,8 @@ const reduxDetailStyles = StyleSheet.create({
1224
1229
  lineHeight: 18,
1225
1230
  },
1226
1231
  highlight: {
1227
- backgroundColor: '#FEF08A',
1228
- color: '#854D0E',
1232
+ backgroundColor: AppColors.yellowHighlight,
1233
+ color: AppColors.amber800Warm,
1229
1234
  fontFamily: AppFonts.interBold,
1230
1235
  },
1231
1236
  });
@@ -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)
@@ -387,14 +395,16 @@ const ReduxTab = React.memo(() => {
387
395
  <View style={reduxTabStyles.searchRow}>
388
396
  <View style={styles.searchContainer}>
389
397
  <SearchIcon color={AppColors.grayTextWeak} size={15}/>
390
- <TextInput placeholder="Search Redux slices or actions..." placeholderTextColor={AppColors.grayTextWeak} value={reduxSearch} onChangeText={setReduxSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
391
- {reduxSearch.length > 0 && (<Pressable onPress={() => setReduxSearch('')} hitSlop={10}>
398
+ <TextInput accessible={true} accessibilityRole="search" accessibilityLabel="Search Redux slices or actions" accessibilityHint="Type to filter state slices in real time" placeholder="Search Redux slices or actions..." placeholderTextColor={AppColors.grayTextWeak} value={reduxSearch} onChangeText={setReduxSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
399
+ {reduxSearch.length > 0 && (<Pressable accessible={true} accessibilityRole="button" accessibilityLabel="Clear search input" onPress={() => setReduxSearch('')} hitSlop={10}>
392
400
  <ClearIcon color={AppColors.grayTextWeak} size={14}/>
393
401
  </Pressable>)}
394
402
  </View>
395
403
 
396
404
  {/* Pause / Resume Live Updates Button */}
397
- <TouchableOpacity style={[
405
+ <TouchableOpacity accessible={true} accessibilityRole="button" accessibilityLabel={isReduxPaused
406
+ ? 'Resume live Redux updates'
407
+ : 'Pause live Redux updates'} accessibilityHint="Toggles automatic recording of Redux state updates" style={[
398
408
  styles.toolbarBtn,
399
409
  isReduxPaused && {
400
410
  backgroundColor: `${AppColors.darkOrange}1F`,
@@ -407,7 +417,9 @@ const ReduxTab = React.memo(() => {
407
417
  </TouchableOpacity>
408
418
 
409
419
  {/* Sort Button */}
410
- <TouchableOpacity style={[
420
+ <TouchableOpacity accessible={true} accessibilityRole="button" accessibilityLabel={`Sort order: ${sortMode === 'latest'
421
+ ? 'Recently updated first'
422
+ : 'Alphabetical A to Z'}`} accessibilityHint="Switches sorting between recently updated and alphabetical" style={[
411
423
  styles.toolbarBtn,
412
424
  sortMode === 'latest' && { backgroundColor: `${AppColors.brandPurple}1F`, borderColor: AppColors.brandPurple },
413
425
  ]} onPress={() => setSortMode(prev => (prev === 'latest' ? 'alpha' : 'latest'))} hitSlop={6}>
@@ -434,7 +446,7 @@ const ReduxTab = React.memo(() => {
434
446
  </View>
435
447
 
436
448
  {/* ─── FlatList of Slices ─── */}
437
- <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={[
438
450
  reduxTabStyles.listContent,
439
451
  filteredSlices.length === 0 && { flexGrow: 1 },
440
452
  ]} keyboardShouldPersistTaps="handled"/>
@@ -539,7 +551,7 @@ const reduxTabStyles = StyleSheet.create({
539
551
  },
540
552
  cardRecentlyUpdated: {
541
553
  borderColor: `${AppColors.brandPurple}66`,
542
- backgroundColor: '#FAFAFF',
554
+ backgroundColor: AppColors.purpleTintBg,
543
555
  },
544
556
  cardHeader: {
545
557
  flexDirection: 'row',
@@ -548,17 +560,17 @@ const reduxTabStyles = StyleSheet.create({
548
560
  marginBottom: 8,
549
561
  },
550
562
  sliceBadge: {
551
- backgroundColor: '#EDE9FE',
563
+ backgroundColor: AppColors.violetSoftBg,
552
564
  paddingHorizontal: 6,
553
565
  paddingVertical: 3,
554
566
  borderRadius: 5,
555
567
  borderWidth: 1,
556
- borderColor: '#DDD6FE',
568
+ borderColor: AppColors.violetSoftBorder,
557
569
  },
558
570
  sliceBadgeText: {
559
571
  fontFamily: AppFonts.interBold,
560
572
  fontSize: 9.5,
561
- color: '#6D28D9',
573
+ color: AppColors.purple700,
562
574
  letterSpacing: 0.4,
563
575
  },
564
576
  sliceName: {