@mcp-shark/mcp-shark 1.4.1 → 1.5.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 (203) hide show
  1. package/README.md +84 -645
  2. package/bin/mcp-shark.js +30 -36
  3. package/mcp-server/index.js +115 -0
  4. package/mcp-server/lib/auditor/audit.js +34 -42
  5. package/mcp-server/lib/common/error.js +1 -1
  6. package/mcp-server/lib/server/external/all.js +5 -6
  7. package/mcp-server/lib/server/external/config.js +1 -3
  8. package/mcp-server/lib/server/external/kv.js +21 -40
  9. package/mcp-server/lib/server/external/single/request.js +3 -6
  10. package/mcp-server/lib/server/external/single/run.js +8 -19
  11. package/mcp-server/lib/server/internal/handlers/prompts-get.js +5 -7
  12. package/mcp-server/lib/server/internal/handlers/prompts-list.js +5 -4
  13. package/mcp-server/lib/server/internal/handlers/resources-list.js +5 -4
  14. package/mcp-server/lib/server/internal/handlers/resources-read.js +5 -4
  15. package/mcp-server/lib/server/internal/handlers/tools-call.js +8 -10
  16. package/mcp-server/lib/server/internal/handlers/tools-list.js +6 -5
  17. package/mcp-server/lib/server/internal/run.js +5 -17
  18. package/mcp-server/lib/server/internal/server.js +14 -15
  19. package/mcp-server/lib/server/internal/session.js +8 -13
  20. package/mcp-server/mcp-shark.js +16 -66
  21. package/package.json +23 -38
  22. package/ui/dist/assets/index-Cc-IUa83.css +1 -0
  23. package/ui/dist/assets/index-srLDlk97.js +35 -0
  24. package/ui/dist/index.html +17 -0
  25. package/ui/dist/og-image.png +0 -0
  26. package/ui/server/routes/backups/deleteBackup.js +54 -0
  27. package/ui/server/routes/backups/index.js +15 -0
  28. package/ui/server/routes/backups/listBackups.js +75 -0
  29. package/ui/server/routes/backups/restoreBackup.js +83 -0
  30. package/ui/server/routes/backups/viewBackup.js +47 -0
  31. package/ui/server/routes/composite/index.js +46 -0
  32. package/ui/server/routes/composite/servers.js +18 -0
  33. package/ui/server/routes/composite/setup.js +129 -0
  34. package/ui/server/routes/composite/status.js +7 -0
  35. package/ui/server/routes/composite/stop.js +39 -0
  36. package/ui/server/routes/composite/utils.js +45 -0
  37. package/ui/server/routes/config.js +34 -30
  38. package/ui/server/routes/conversations.js +3 -3
  39. package/ui/server/routes/help.js +2 -2
  40. package/ui/server/routes/logs.js +5 -5
  41. package/ui/server/routes/playground.js +91 -62
  42. package/ui/server/routes/requests.js +112 -108
  43. package/ui/server/routes/sessions.js +4 -4
  44. package/ui/server/routes/settings.js +199 -0
  45. package/ui/server/routes/smartscan/discover.js +7 -6
  46. package/ui/server/routes/smartscan/scans/clearCache.js +3 -2
  47. package/ui/server/routes/smartscan/scans/createBatchScans.js +4 -3
  48. package/ui/server/routes/smartscan/scans/createScan.js +2 -1
  49. package/ui/server/routes/smartscan/scans/getCachedResults.js +2 -1
  50. package/ui/server/routes/smartscan/scans/getScan.js +2 -1
  51. package/ui/server/routes/smartscan/scans/listScans.js +5 -4
  52. package/ui/server/routes/smartscan/scans.js +3 -3
  53. package/ui/server/routes/smartscan/token.js +4 -3
  54. package/ui/server/routes/smartscan/transport.js +1 -1
  55. package/ui/server/routes/smartscan.js +1 -1
  56. package/ui/server/routes/statistics.js +13 -10
  57. package/ui/server/utils/config-update.js +140 -112
  58. package/ui/server/utils/config.js +4 -4
  59. package/ui/server/utils/logger.js +2 -0
  60. package/ui/server/utils/paths.js +210 -2
  61. package/ui/server/utils/port.js +2 -2
  62. package/ui/server/utils/process.js +0 -67
  63. package/ui/server/utils/scan-cache/all-results.js +76 -59
  64. package/ui/server/utils/scan-cache/directory.js +1 -1
  65. package/ui/server/utils/scan-cache/file-operations.js +19 -16
  66. package/ui/server/utils/scan-cache/server-operations.js +14 -9
  67. package/ui/server/utils/serialization.js +9 -3
  68. package/ui/server/utils/smartscan-token.js +4 -3
  69. package/ui/server.js +87 -41
  70. package/ui/src/App.jsx +5 -5
  71. package/ui/src/CompositeLogs.jsx +20 -20
  72. package/ui/src/CompositeSetup.jsx +9 -9
  73. package/ui/src/HelpGuide/HelpGuideFooter.jsx +1 -0
  74. package/ui/src/HelpGuide/HelpGuideHeader.jsx +2 -1
  75. package/ui/src/HelpGuide.jsx +17 -4
  76. package/ui/src/IntroTour.jsx +19 -5
  77. package/ui/src/LogDetail.jsx +1 -0
  78. package/ui/src/LogTable.jsx +24 -6
  79. package/ui/src/PacketDetail.jsx +21 -16
  80. package/ui/src/PacketFilters.jsx +29 -14
  81. package/ui/src/PacketList.jsx +4 -5
  82. package/ui/src/SmartScan.jsx +5 -5
  83. package/ui/src/TabNavigation.jsx +5 -5
  84. package/ui/src/components/App/HelpButton.jsx +4 -0
  85. package/ui/src/components/App/TrafficTab.jsx +4 -4
  86. package/ui/src/components/App/useAppState.js +118 -24
  87. package/ui/src/components/BackupList.jsx +6 -2
  88. package/ui/src/components/CollapsibleSection.jsx +16 -2
  89. package/ui/src/components/ConfigViewerModal.jsx +17 -3
  90. package/ui/src/components/ConfirmationModal.jsx +20 -3
  91. package/ui/src/components/DetailsTab/BodySection.jsx +3 -1
  92. package/ui/src/components/DetailsTab/CollapsibleRequestResponse.jsx +14 -3
  93. package/ui/src/components/DetailsTab/InfoSection.jsx +4 -2
  94. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +7 -5
  95. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +7 -5
  96. package/ui/src/components/DetectedPathsList.jsx +5 -2
  97. package/ui/src/components/FileInput.jsx +3 -1
  98. package/ui/src/components/GroupHeader.jsx +14 -0
  99. package/ui/src/components/GroupedByMcpView.jsx +3 -10
  100. package/ui/src/components/GroupedByServerView.jsx +1 -1
  101. package/ui/src/components/GroupedBySessionView.jsx +1 -1
  102. package/ui/src/components/HexTab.jsx +17 -4
  103. package/ui/src/components/LogsToolbar.jsx +3 -1
  104. package/ui/src/components/McpPlayground/LoadingModal.jsx +7 -3
  105. package/ui/src/components/McpPlayground/PromptsSection/PromptCallPanel.jsx +5 -0
  106. package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +52 -23
  107. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +13 -11
  108. package/ui/src/components/McpPlayground/PromptsSection.jsx +2 -1
  109. package/ui/src/components/McpPlayground/ResourcesSection/ResourceCallPanel.jsx +3 -0
  110. package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +66 -34
  111. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +13 -11
  112. package/ui/src/components/McpPlayground/ResourcesSection.jsx +2 -1
  113. package/ui/src/components/McpPlayground/ToolsSection/ToolCallPanel.jsx +5 -0
  114. package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +52 -23
  115. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +13 -11
  116. package/ui/src/components/McpPlayground/ToolsSection.jsx +2 -1
  117. package/ui/src/components/McpPlayground/hooks/useMcpDataLoader.js +107 -0
  118. package/ui/src/components/McpPlayground/hooks/useMcpRequest.js +70 -0
  119. package/ui/src/components/McpPlayground/hooks/useMcpServerStatus.js +90 -0
  120. package/ui/src/components/McpPlayground/useMcpPlayground.js +118 -159
  121. package/ui/src/components/McpPlayground.jsx +105 -23
  122. package/ui/src/components/PacketDetailHeader.jsx +8 -3
  123. package/ui/src/components/PacketFilters/ExportControls.jsx +2 -1
  124. package/ui/src/components/PacketFilters/FilterInput.jsx +1 -1
  125. package/ui/src/components/RawTab.jsx +15 -2
  126. package/ui/src/components/RequestRow/OrphanedResponseRow.jsx +10 -2
  127. package/ui/src/components/RequestRow/RequestRowMain.jsx +12 -3
  128. package/ui/src/components/RequestRow/ResponseRow.jsx +11 -3
  129. package/ui/src/components/RequestRow.jsx +17 -9
  130. package/ui/src/components/ServerControl.jsx +3 -1
  131. package/ui/src/components/ServiceSelector.jsx +2 -0
  132. package/ui/src/components/SmartScan/AnalysisResult.jsx +2 -2
  133. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultItem.jsx +2 -1
  134. package/ui/src/components/SmartScan/BatchResultsDisplay/BatchResultsHeader.jsx +1 -1
  135. package/ui/src/components/SmartScan/BatchResultsDisplay.jsx +9 -3
  136. package/ui/src/components/SmartScan/EmptyState.jsx +3 -0
  137. package/ui/src/components/SmartScan/ErrorDisplay.jsx +4 -2
  138. package/ui/src/components/SmartScan/ExpandableSection.jsx +4 -0
  139. package/ui/src/components/SmartScan/FindingsTable.jsx +46 -42
  140. package/ui/src/components/SmartScan/ListViewContent.jsx +2 -2
  141. package/ui/src/components/SmartScan/NotablePatternsSection.jsx +13 -8
  142. package/ui/src/components/SmartScan/OverallSummarySection.jsx +36 -29
  143. package/ui/src/components/SmartScan/RecommendationsSection.jsx +10 -8
  144. package/ui/src/components/SmartScan/ScanDetailHeader.jsx +2 -1
  145. package/ui/src/components/SmartScan/ScanDetailView.jsx +4 -4
  146. package/ui/src/components/SmartScan/ScanListView/ScanListHeader.jsx +2 -1
  147. package/ui/src/components/SmartScan/ScanListView/ScanListItem.jsx +15 -1
  148. package/ui/src/components/SmartScan/ScanOverviewSection.jsx +3 -1
  149. package/ui/src/components/SmartScan/ScanResultsDisplay.jsx +1 -1
  150. package/ui/src/components/SmartScan/ScanViewContent.jsx +2 -2
  151. package/ui/src/components/SmartScan/ScanningProgress.jsx +4 -2
  152. package/ui/src/components/SmartScan/ServerInfoSection.jsx +3 -1
  153. package/ui/src/components/SmartScan/ServerSelectionRow.jsx +4 -2
  154. package/ui/src/components/SmartScan/SingleResultDisplay.jsx +5 -3
  155. package/ui/src/components/SmartScan/SmartScanControls.jsx +11 -7
  156. package/ui/src/components/SmartScan/SmartScanHeader.jsx +1 -1
  157. package/ui/src/components/SmartScan/ViewModeTabs.jsx +2 -0
  158. package/ui/src/components/SmartScan/hooks/useCacheManagement.js +1 -2
  159. package/ui/src/components/SmartScan/hooks/useMcpDiscovery.js +22 -26
  160. package/ui/src/components/SmartScan/hooks/useScanList.js +10 -9
  161. package/ui/src/components/SmartScan/hooks/useScanOperations.js +23 -14
  162. package/ui/src/components/SmartScan/hooks/useServerStatus.js +2 -2
  163. package/ui/src/components/SmartScan/hooks/useTokenManagement.js +2 -2
  164. package/ui/src/components/SmartScan/scanDataUtils.js +22 -17
  165. package/ui/src/components/SmartScan/useSmartScan.js +4 -4
  166. package/ui/src/components/SmartScan/utils.js +3 -1
  167. package/ui/src/components/SmartScanIcons.jsx +6 -3
  168. package/ui/src/components/TabNavigation/DesktopTabs.jsx +8 -3
  169. package/ui/src/components/TabNavigation/MobileDropdown.jsx +3 -1
  170. package/ui/src/components/TabNavigation.jsx +8 -3
  171. package/ui/src/components/TabNavigationIcons.jsx +4 -4
  172. package/ui/src/components/TourOverlay.jsx +1 -1
  173. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +3 -0
  174. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +1 -0
  175. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +9 -0
  176. package/ui/src/components/TourTooltip/useTooltipPosition.js +63 -36
  177. package/ui/src/components/TourTooltip.jsx +11 -3
  178. package/ui/src/components/ViewModeTabs.jsx +3 -1
  179. package/ui/src/config/tourSteps.jsx +0 -2
  180. package/ui/src/hooks/useAnimation.js +15 -12
  181. package/ui/src/hooks/useConfigManagement.js +8 -8
  182. package/ui/src/hooks/useServiceExtraction.js +1 -1
  183. package/ui/src/index.css +3 -8
  184. package/ui/src/theme.js +3 -3
  185. package/ui/src/utils/hexUtils.js +11 -5
  186. package/ui/src/utils/mcpGroupingUtils.js +18 -10
  187. package/ui/src/utils/requestPairing.js +89 -0
  188. package/ui/src/utils/requestUtils.js +37 -105
  189. package/ui/vite.config.js +1 -1
  190. package/mcp-server/.editorconfig +0 -15
  191. package/mcp-server/.prettierignore +0 -11
  192. package/mcp-server/.prettierrc +0 -12
  193. package/mcp-server/README.md +0 -280
  194. package/mcp-server/commitlint.config.cjs +0 -42
  195. package/mcp-server/eslint.config.js +0 -131
  196. package/mcp-server/package-lock.json +0 -4784
  197. package/mcp-server/package.json +0 -30
  198. package/ui/README.md +0 -212
  199. package/ui/package-lock.json +0 -3574
  200. package/ui/package.json +0 -12
  201. package/ui/paths.js +0 -282
  202. package/ui/server/routes/backups.js +0 -251
  203. package/ui/server/routes/composite.js +0 -244
@@ -1,9 +1,9 @@
1
1
  import { colors, fonts } from '../theme';
2
- import { useMcpPlayground } from './McpPlayground/useMcpPlayground';
3
2
  import LoadingModal from './McpPlayground/LoadingModal';
4
- import ToolsSection from './McpPlayground/ToolsSection';
5
3
  import PromptsSection from './McpPlayground/PromptsSection';
6
4
  import ResourcesSection from './McpPlayground/ResourcesSection';
5
+ import ToolsSection from './McpPlayground/ToolsSection';
6
+ import { useMcpPlayground } from './McpPlayground/useMcpPlayground';
7
7
 
8
8
  function McpPlayground() {
9
9
  const {
@@ -41,6 +41,9 @@ function McpPlayground() {
41
41
  handleCallTool,
42
42
  handleGetPrompt,
43
43
  handleReadResource,
44
+ availableServers,
45
+ selectedServer,
46
+ setSelectedServer,
44
47
  } = useMcpPlayground();
45
48
 
46
49
  return (
@@ -84,32 +87,111 @@ function McpPlayground() {
84
87
  <div
85
88
  style={{
86
89
  display: 'flex',
87
- gap: '8px',
88
- borderBottom: `1px solid ${colors.borderLight}`,
90
+ flexDirection: 'column',
91
+ gap: '12px',
89
92
  }}
90
93
  >
91
- {['tools', 'prompts', 'resources'].map((section) => (
92
- <button
93
- key={section}
94
- onClick={() => setActiveSection(section)}
94
+ {availableServers.length > 0 && (
95
+ <div
95
96
  style={{
96
- padding: '10px 18px',
97
- background: activeSection === section ? colors.bgSecondary : 'transparent',
98
- border: 'none',
99
- borderBottom: `2px solid ${activeSection === section ? colors.accentBlue : 'transparent'}`,
100
- color: activeSection === section ? colors.textPrimary : colors.textSecondary,
101
- cursor: 'pointer',
102
- fontSize: '13px',
103
- fontFamily: fonts.body,
104
- fontWeight: activeSection === section ? '500' : '400',
105
- textTransform: 'capitalize',
106
- borderRadius: '6px 6px 0 0',
107
- transition: 'all 0.2s',
97
+ display: 'flex',
98
+ flexDirection: 'column',
99
+ gap: '8px',
108
100
  }}
109
101
  >
110
- {section}
111
- </button>
112
- ))}
102
+ <label
103
+ htmlFor="mcp-server-select"
104
+ style={{
105
+ fontSize: '13px',
106
+ fontFamily: fonts.body,
107
+ color: colors.textSecondary,
108
+ fontWeight: '500',
109
+ }}
110
+ >
111
+ Server:
112
+ </label>
113
+ <div
114
+ style={{
115
+ display: 'flex',
116
+ flexWrap: 'wrap',
117
+ gap: '8px',
118
+ }}
119
+ >
120
+ {availableServers.map((server) => (
121
+ <button
122
+ key={server}
123
+ type="button"
124
+ onClick={() => setSelectedServer(server)}
125
+ style={{
126
+ padding: '10px 18px',
127
+ background:
128
+ selectedServer === server ? colors.accentBlue : colors.bgSecondary,
129
+ border:
130
+ selectedServer === server
131
+ ? `2px solid ${colors.accentBlue}`
132
+ : `1px solid ${colors.borderLight}`,
133
+ borderRadius: '8px',
134
+ color: selectedServer === server ? colors.textInverse : colors.textPrimary,
135
+ fontSize: '13px',
136
+ fontFamily: fonts.body,
137
+ fontWeight: selectedServer === server ? '600' : '500',
138
+ cursor: 'pointer',
139
+ transition: 'all 0.2s ease',
140
+ boxShadow:
141
+ selectedServer === server ? `0 2px 4px ${colors.shadowSm}` : 'none',
142
+ }}
143
+ onMouseEnter={(e) => {
144
+ if (selectedServer !== server) {
145
+ e.currentTarget.style.background = colors.bgHover;
146
+ e.currentTarget.style.borderColor = colors.borderMedium;
147
+ e.currentTarget.style.boxShadow = `0 2px 4px ${colors.shadowSm}`;
148
+ }
149
+ }}
150
+ onMouseLeave={(e) => {
151
+ if (selectedServer !== server) {
152
+ e.currentTarget.style.background = colors.bgSecondary;
153
+ e.currentTarget.style.borderColor = colors.borderLight;
154
+ e.currentTarget.style.boxShadow = 'none';
155
+ }
156
+ }}
157
+ >
158
+ {server}
159
+ </button>
160
+ ))}
161
+ </div>
162
+ </div>
163
+ )}
164
+ <div
165
+ style={{
166
+ display: 'flex',
167
+ gap: '8px',
168
+ borderBottom: `1px solid ${colors.borderLight}`,
169
+ }}
170
+ >
171
+ {['tools', 'prompts', 'resources'].map((section) => (
172
+ <button
173
+ key={section}
174
+ type="button"
175
+ onClick={() => setActiveSection(section)}
176
+ style={{
177
+ padding: '10px 18px',
178
+ background: activeSection === section ? colors.bgSecondary : 'transparent',
179
+ border: 'none',
180
+ borderBottom: `2px solid ${activeSection === section ? colors.accentBlue : 'transparent'}`,
181
+ color: activeSection === section ? colors.textPrimary : colors.textSecondary,
182
+ cursor: 'pointer',
183
+ fontSize: '13px',
184
+ fontFamily: fonts.body,
185
+ fontWeight: activeSection === section ? '500' : '400',
186
+ textTransform: 'capitalize',
187
+ borderRadius: '6px 6px 0 0',
188
+ transition: 'all 0.2s',
189
+ }}
190
+ >
191
+ {section}
192
+ </button>
193
+ ))}
194
+ </div>
113
195
  </div>
114
196
 
115
197
  <div style={{ flex: 1, overflow: 'hidden', minHeight: 0 }}>
@@ -1,10 +1,14 @@
1
- import { colors, fonts } from '../theme';
2
1
  import { IconX } from '@tabler/icons-react';
2
+ import { colors, fonts } from '../theme';
3
3
 
4
4
  function PacketDetailHeader({ request, onClose, matchingPair }) {
5
5
  const formatBytes = (bytes) => {
6
- if (bytes < 1024) return `${bytes} B`;
7
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
6
+ if (bytes < 1024) {
7
+ return `${bytes} B`;
8
+ }
9
+ if (bytes < 1024 * 1024) {
10
+ return `${(bytes / 1024).toFixed(2)} KB`;
11
+ }
8
12
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
9
13
  };
10
14
 
@@ -59,6 +63,7 @@ function PacketDetailHeader({ request, onClose, matchingPair }) {
59
63
  </span>
60
64
  </div>
61
65
  <button
66
+ type="button"
62
67
  onClick={onClose}
63
68
  style={{
64
69
  background: 'none',
@@ -1,6 +1,6 @@
1
- import { colors, fonts } from '../../theme';
2
1
  import { IconDownload } from '@tabler/icons-react';
3
2
  import anime from 'animejs';
3
+ import { colors, fonts } from '../../theme';
4
4
 
5
5
  export default function ExportControls({ stats, onExport }) {
6
6
  return (
@@ -50,6 +50,7 @@ export default function ExportControls({ stats, onExport }) {
50
50
  }}
51
51
  >
52
52
  <button
53
+ type="button"
53
54
  onClick={() => onExport('json')}
54
55
  style={{
55
56
  padding: '8px 14px',
@@ -1,5 +1,5 @@
1
- import { colors, fonts } from '../../theme';
2
1
  import anime from 'animejs';
2
+ import { colors, fonts } from '../../theme';
3
3
 
4
4
  export default function FilterInput({
5
5
  type = 'text',
@@ -17,7 +17,10 @@ const ChevronDown = ({ size = 14, color = 'currentColor', rotated = false }) =>
17
17
  transform: rotated ? 'rotate(-90deg)' : 'rotate(0deg)',
18
18
  transition: 'transform 0.2s ease',
19
19
  }}
20
+ role="img"
21
+ aria-label="Chevron down icon"
20
22
  >
23
+ <title>Chevron down icon</title>
21
24
  <polyline points="6 9 12 15 18 9" />
22
25
  </svg>
23
26
  );
@@ -35,8 +38,15 @@ function CollapsibleRequestResponse({ title, titleColor, children, defaultExpand
35
38
  marginBottom: '20px',
36
39
  }}
37
40
  >
38
- <div
41
+ <button
42
+ type="button"
39
43
  onClick={() => setIsExpanded(!isExpanded)}
44
+ onKeyDown={(e) => {
45
+ if (e.key === 'Enter' || e.key === ' ') {
46
+ e.preventDefault();
47
+ setIsExpanded(!isExpanded);
48
+ }
49
+ }}
40
50
  style={{
41
51
  padding: '16px 20px',
42
52
  background: isExpanded ? colors.bgCard : colors.bgSecondary,
@@ -47,6 +57,9 @@ function CollapsibleRequestResponse({ title, titleColor, children, defaultExpand
47
57
  alignItems: 'center',
48
58
  justifyContent: 'space-between',
49
59
  transition: 'background-color 0.15s ease',
60
+ width: '100%',
61
+ border: 'none',
62
+ textAlign: 'left',
50
63
  }}
51
64
  onMouseEnter={(e) => {
52
65
  e.currentTarget.style.background = colors.bgHover;
@@ -71,7 +84,7 @@ function CollapsibleRequestResponse({ title, titleColor, children, defaultExpand
71
84
  <ChevronDown size={14} color={titleColor} rotated={!isExpanded} />
72
85
  {title}
73
86
  </div>
74
- </div>
87
+ </button>
75
88
  {isExpanded && <div style={{ padding: '20px' }}>{children}</div>}
76
89
  </div>
77
90
  );
@@ -1,9 +1,9 @@
1
1
  import { colors, fonts, withOpacity } from '../../theme';
2
2
  import {
3
- formatRelativeTime,
4
3
  formatDateTime,
5
- getSourceDest,
4
+ formatRelativeTime,
6
5
  getEndpoint,
6
+ getSourceDest,
7
7
  } from '../../utils/requestUtils.js';
8
8
 
9
9
  export default function OrphanedResponseRow({ response, selected, firstRequestTime, onSelect }) {
@@ -14,6 +14,14 @@ export default function OrphanedResponseRow({ response, selected, firstRequestTi
14
14
  return (
15
15
  <tr
16
16
  onClick={() => onSelect(response)}
17
+ onKeyDown={(e) => {
18
+ if (e.key === 'Enter' || e.key === ' ') {
19
+ e.preventDefault();
20
+ onSelect(response);
21
+ }
22
+ }}
23
+ tabIndex={0}
24
+ aria-label={`Select orphaned response ${response.frame_number}`}
17
25
  style={{
18
26
  cursor: 'pointer',
19
27
  background: isSelected ? colors.bgSelected : colors.bgUnpaired,
@@ -1,11 +1,11 @@
1
+ import { IconChevronDown } from '@tabler/icons-react';
1
2
  import { colors, fonts, withOpacity } from '../../theme';
2
3
  import {
3
- formatRelativeTime,
4
4
  formatDateTime,
5
- getSourceDest,
5
+ formatRelativeTime,
6
6
  getEndpoint,
7
+ getSourceDest,
7
8
  } from '../../utils/requestUtils.js';
8
- import { IconChevronDown } from '@tabler/icons-react';
9
9
 
10
10
  const ChevronDown = ({ size = 12, rotated = false }) => (
11
11
  <IconChevronDown
@@ -39,6 +39,14 @@ export default function RequestRowMain({
39
39
  <>
40
40
  <tr
41
41
  onClick={() => onSelect(request)}
42
+ onKeyDown={(e) => {
43
+ if (e.key === 'Enter' || e.key === ' ') {
44
+ e.preventDefault();
45
+ onSelect(request);
46
+ }
47
+ }}
48
+ tabIndex={0}
49
+ aria-label={`Select request ${request.frame_number}`}
42
50
  style={{
43
51
  cursor: 'pointer',
44
52
  background: isSelected
@@ -84,6 +92,7 @@ export default function RequestRowMain({
84
92
  >
85
93
  {hasResponse && (
86
94
  <button
95
+ type="button"
87
96
  onClick={(e) => {
88
97
  e.stopPropagation();
89
98
  onToggleExpand();
@@ -1,12 +1,12 @@
1
1
  import { colors, fonts } from '../../theme';
2
2
  import {
3
- formatRelativeTime,
4
3
  formatDateTime,
5
- getSourceDest,
4
+ formatRelativeTime,
6
5
  getEndpoint,
6
+ getSourceDest,
7
7
  } from '../../utils/requestUtils.js';
8
8
 
9
- export default function ResponseRow({ response, selected, firstRequestTime, onSelect, request }) {
9
+ export default function ResponseRow({ response, selected, firstRequestTime, onSelect }) {
10
10
  const isSelected = selected?.frame_number === response.frame_number;
11
11
  const { source, dest } = getSourceDest(response);
12
12
  const relativeTime = formatRelativeTime(response.timestamp_iso, firstRequestTime);
@@ -14,6 +14,14 @@ export default function ResponseRow({ response, selected, firstRequestTime, onSe
14
14
  return (
15
15
  <tr
16
16
  onClick={() => onSelect(response)}
17
+ onKeyDown={(e) => {
18
+ if (e.key === 'Enter' || e.key === ' ') {
19
+ e.preventDefault();
20
+ onSelect(response);
21
+ }
22
+ }}
23
+ tabIndex={0}
24
+ aria-label={`Select response ${response.frame_number}`}
17
25
  style={{
18
26
  cursor: 'pointer',
19
27
  background:
@@ -12,17 +12,23 @@ function RequestRow({
12
12
  onToggleExpand = () => {},
13
13
  }) {
14
14
  // Support both pair prop (new) and request prop (legacy for grouped views)
15
- let request, response;
16
- if (pair) {
17
- request = pair.request;
18
- response = pair.response;
19
- } else if (requestProp) {
20
- request = requestProp;
21
- response = null;
22
- } else {
15
+ const extractRequestResponse = (pair, requestProp) => {
16
+ if (pair) {
17
+ return { request: pair.request, response: pair.response };
18
+ }
19
+ if (requestProp) {
20
+ return { request: requestProp, response: null };
21
+ }
22
+ return null;
23
+ };
24
+
25
+ const data = extractRequestResponse(pair, requestProp);
26
+ if (!data) {
23
27
  return null; // No valid data
24
28
  }
25
29
 
30
+ const { request, response } = data;
31
+
26
32
  // Check if this is an unpaired request or response
27
33
  const isUnpaired = !request || !response;
28
34
 
@@ -38,7 +44,9 @@ function RequestRow({
38
44
  );
39
45
  }
40
46
 
41
- if (!request) return null; // Only show rows that have a request
47
+ if (!request) {
48
+ return null; // Only show rows that have a request
49
+ }
42
50
 
43
51
  const hasResponse = !!response;
44
52
 
@@ -29,6 +29,7 @@ function ServerControl({ status, loading, onStart, onStop, canStart }) {
29
29
  <div style={{ display: 'flex', gap: '12px', alignItems: 'center', flexWrap: 'wrap' }}>
30
30
  {status.running ? (
31
31
  <button
32
+ type="button"
32
33
  onClick={onStop}
33
34
  disabled={loading}
34
35
  style={{
@@ -60,6 +61,7 @@ function ServerControl({ status, loading, onStart, onStop, canStart }) {
60
61
  </button>
61
62
  ) : (
62
63
  <button
64
+ type="button"
63
65
  data-tour="start-button"
64
66
  onClick={onStart}
65
67
  disabled={loading || !canStart}
@@ -122,7 +124,7 @@ function ServerControl({ status, loading, onStart, onStop, canStart }) {
122
124
  fontFamily: fonts.body,
123
125
  }}
124
126
  >
125
- {status.running ? `Running (PID: ${status.pid})` : 'Stopped'}
127
+ {status.running ? (status.pid ? `Running (PID: ${status.pid})` : 'Running') : 'Stopped'}
126
128
  </span>
127
129
  </div>
128
130
  </div>
@@ -40,6 +40,7 @@ function ServiceSelector({ services, selectedServices, onSelectionChange }) {
40
40
  }}
41
41
  >
42
42
  <button
43
+ type="button"
43
44
  onClick={() => {
44
45
  onSelectionChange(new Set(services.map((s) => s.name)));
45
46
  }}
@@ -56,6 +57,7 @@ function ServiceSelector({ services, selectedServices, onSelectionChange }) {
56
57
  Select All
57
58
  </button>
58
59
  <button
60
+ type="button"
59
61
  onClick={() => {
60
62
  onSelectionChange(new Set());
61
63
  }}
@@ -1,9 +1,9 @@
1
1
  import { colors, fonts } from '../../theme';
2
2
  import ExpandableSection from './ExpandableSection';
3
3
  import FindingsTable from './FindingsTable';
4
+ import NotablePatternsSection from './NotablePatternsSection';
4
5
  import OverallSummarySection from './OverallSummarySection';
5
6
  import RecommendationsSection from './RecommendationsSection';
6
- import NotablePatternsSection from './NotablePatternsSection';
7
7
 
8
8
  export default function AnalysisResult({ analysis }) {
9
9
  if (!analysis) {
@@ -11,7 +11,7 @@ export default function AnalysisResult({ analysis }) {
11
11
  <div
12
12
  style={{
13
13
  padding: '12px',
14
- background: colors.bgTertiary + '80',
14
+ background: `${colors.bgTertiary}80`,
15
15
  borderRadius: '6px',
16
16
  border: `1px solid ${colors.borderLight}`,
17
17
  fontSize: '12px',
@@ -1,6 +1,6 @@
1
- import { CheckIcon, CacheIcon, ExternalLinkIcon } from '../../SmartScanIcons';
2
1
  import { IconEye } from '@tabler/icons-react';
3
2
  import { colors, fonts } from '../../../theme';
3
+ import { CacheIcon, CheckIcon, ExternalLinkIcon } from '../../SmartScanIcons';
4
4
  import { getRiskLevelColor } from '../utils';
5
5
 
6
6
  export default function BatchResultItem({ result, onViewScan }) {
@@ -64,6 +64,7 @@ export default function BatchResultItem({ result, onViewScan }) {
64
64
  )}
65
65
  {onViewScan && result.data && (
66
66
  <button
67
+ type="button"
67
68
  onClick={() => onViewScan(result.data)}
68
69
  style={{
69
70
  display: 'inline-flex',
@@ -1,5 +1,5 @@
1
- import { CheckIcon, CacheIcon } from '../../SmartScanIcons';
2
1
  import { colors, fonts } from '../../../theme';
2
+ import { CacheIcon, CheckIcon } from '../../SmartScanIcons';
3
3
 
4
4
  export default function BatchResultsHeader({ scanResults }) {
5
5
  const cachedCount = scanResults.filter((r) => r.cached).length;
@@ -1,9 +1,11 @@
1
1
  import { colors } from '../../theme';
2
- import BatchResultsHeader from './BatchResultsDisplay/BatchResultsHeader';
3
2
  import BatchResultItem from './BatchResultsDisplay/BatchResultItem';
3
+ import BatchResultsHeader from './BatchResultsDisplay/BatchResultsHeader';
4
4
 
5
5
  export default function BatchResultsDisplay({ scanResults, onViewScan }) {
6
- if (scanResults.length === 0) return null;
6
+ if (scanResults.length === 0) {
7
+ return null;
8
+ }
7
9
 
8
10
  return (
9
11
  <div
@@ -18,7 +20,11 @@ export default function BatchResultsDisplay({ scanResults, onViewScan }) {
18
20
  <BatchResultsHeader scanResults={scanResults} />
19
21
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
20
22
  {scanResults.map((result, idx) => (
21
- <BatchResultItem key={idx} result={result} onViewScan={onViewScan} />
23
+ <BatchResultItem
24
+ key={result.scanId || `batch-result-${idx}`}
25
+ result={result}
26
+ onViewScan={onViewScan}
27
+ />
22
28
  ))}
23
29
  </div>
24
30
  </div>
@@ -24,7 +24,10 @@ export default function EmptyState() {
24
24
  strokeLinecap="round"
25
25
  strokeLinejoin="round"
26
26
  style={{ opacity: 0.5 }}
27
+ role="img"
28
+ aria-label="Empty state icon"
27
29
  >
30
+ <title>Empty state icon</title>
28
31
  <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
29
32
  <path d="M9 12l2 2 4-4" />
30
33
  </svg>
@@ -1,8 +1,10 @@
1
- import { AlertIcon } from '../SmartScanIcons';
2
1
  import { colors, fonts } from '../../theme';
2
+ import { AlertIcon } from '../SmartScanIcons';
3
3
 
4
4
  export default function ErrorDisplay({ error }) {
5
- if (!error) return null;
5
+ if (!error) {
6
+ return null;
7
+ }
6
8
 
7
9
  return (
8
10
  <div
@@ -14,6 +14,7 @@ export default function ExpandableSection({ title, count, children, defaultExpan
14
14
  }}
15
15
  >
16
16
  <button
17
+ type="button"
17
18
  onClick={() => setIsExpanded(!isExpanded)}
18
19
  style={{
19
20
  width: '100%',
@@ -73,7 +74,10 @@ export default function ExpandableSection({ title, count, children, defaultExpan
73
74
  fill="none"
74
75
  stroke="currentColor"
75
76
  viewBox="0 0 24 24"
77
+ role="img"
78
+ aria-label="Chevron icon"
76
79
  >
80
+ <title>Chevron icon</title>
77
81
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
78
82
  </svg>
79
83
  </button>