@qiaolei81/copilot-session-viewer 0.1.9 → 0.2.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 (56) hide show
  1. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-be-responsive-on-mobile-viewport-1771605454041.json +435 -0
  2. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-display-sessions-if-available-1771605462872.json +435 -0
  3. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-JavaScript-errors-gracefully-1771605463381.json +435 -0
  4. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-session-import-dialog-1771605466264.json +435 -0
  5. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-have-working-infinite-scroll-elements-1771605454038.json +435 -0
  6. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-homepage-with-basic-elements-1771605454001.json +435 -0
  7. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-time-analysis-page-1771605464990.json +1236 -0
  8. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-navigate-to-session-detail-page-1771605472595.json +1177 -0
  9. package/.nyc_output/coverage-e2e-merged.json +1 -0
  10. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-display-session-list-1771605453565.json +435 -0
  11. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-load-homepage-successfully-1771605453552.json +435 -0
  12. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-navigate-to-session-detail-on-click-1771605469317.json +1134 -0
  13. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-show-session-metadata-1771605460581.json +435 -0
  14. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-display-Load-More-Sessions-button-when-there-are-more-sessions-1771605468486.json +435 -0
  15. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-handle-API-errors-gracefully-during-infinite-scroll-1771605482161.json +471 -0
  16. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-hide-Load-More-button-when-no-more-sessions-available-1771605478370.json +471 -0
  17. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-load-additional-sessions-when-Load-More-button-is-clicked-1771605475059.json +471 -0
  18. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-preserve-session-list-state-during-navigation-1771605494575.json +1633 -0
  19. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-show-loading-state-when-Load-More-button-is-clicked-1771605475401.json +471 -0
  20. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-trigger-infinite-scroll-when-scrolling-near-bottom-1771605476949.json +471 -0
  21. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-clear-search-filter-1771605508542.json +1255 -0
  22. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-event-list-1771605505572.json +1156 -0
  23. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-session-metadata-1771605504552.json +701 -0
  24. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-expand-and-collapse-tool-details-1771605515809.json +1182 -0
  25. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-filter-events-by-search-1771605513421.json +1245 -0
  26. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-load-session-detail-page-1771605494974.json +701 -0
  27. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-toggle-content-visibility-1771605550729.json +1177 -0
  28. package/.nyc_output/coverage-unit.json +21 -0
  29. package/.nycrc +29 -0
  30. package/CHANGELOG.md +36 -0
  31. package/README.md +154 -15
  32. package/examples/parser-usage.js +114 -0
  33. package/lib/parsers/README.md +239 -0
  34. package/lib/parsers/base-parser.js +53 -0
  35. package/lib/parsers/claude-parser.js +181 -0
  36. package/lib/parsers/copilot-parser.js +143 -0
  37. package/lib/parsers/index.js +13 -0
  38. package/lib/parsers/parser-factory.js +77 -0
  39. package/lib/parsers/pi-mono-parser.js +119 -0
  40. package/package.json +12 -4
  41. package/server.js +17 -2
  42. package/src/app.js +45 -20
  43. package/src/controllers/insightController.js +44 -8
  44. package/src/controllers/sessionController.js +217 -3
  45. package/src/controllers/uploadController.js +447 -7
  46. package/src/middleware/rateLimiting.js +7 -1
  47. package/src/models/Session.js +26 -0
  48. package/src/schemas/event.schema.js +73 -0
  49. package/src/services/eventNormalizer.js +291 -0
  50. package/src/services/insightService.js +140 -48
  51. package/src/services/sessionRepository.js +584 -49
  52. package/src/services/sessionService.js +1588 -36
  53. package/src/utils/helpers.js +6 -1
  54. package/views/index.ejs +111 -4
  55. package/views/session-vue.ejs +272 -65
  56. package/views/time-analyze.ejs +127 -55
@@ -0,0 +1,435 @@
1
+ [
2
+ {
3
+ "url": "http://localhost:3838/",
4
+ "scriptId": "4",
5
+ "source": "\n // Parse JSON data safely from script tags\n const initialSessions = JSON.parse(document.getElementById('sessions-data').textContent);\n const metaData = JSON.parse(document.getElementById('meta-data').textContent);\n const totalSessionsFromServer = metaData.totalSessions;\n const hasMoreFromServer = metaData.hasMore;\n\n // Infinite scroll state\n let allSessions = [...initialSessions];\n let currentOffset = initialSessions.length;\n let hasMore = hasMoreFromServer;\n let isLoading = false;\n\n // Filter state\n let currentSourceFilter = 'all';\n // Load more sessions\n async function loadMoreSessions() {\n if (isLoading || !hasMore) return;\n\n isLoading = true;\n const loadMoreBtn = document.getElementById('load-more-btn');\n const loadingIndicator = document.getElementById('loading-indicator');\n const loadMoreSection = document.getElementById('load-more-section');\n\n // Show loading, hide button\n loadMoreSection.style.display = 'none';\n loadingIndicator.style.display = 'block';\n\n try {\n const response = await fetch(`/api/sessions/load-more?offset=${currentOffset}&limit=20`);\n if (!response.ok) {\n throw new Error('Failed to load more sessions');\n }\n\n const data = await response.json();\n allSessions.push(...data.sessions);\n currentOffset += data.sessions.length;\n hasMore = data.hasMore;\n\n // Re-render all sessions\n renderAllSessions();\n updateLoadMoreButton();\n\n } catch (err) {\n console.error('Error loading more sessions:', err);\n // Show button again on error\n loadMoreSection.style.display = hasMore ? 'block' : 'none';\n } finally {\n isLoading = false;\n loadingIndicator.style.display = 'none';\n }\n }\n\n // Update load more button visibility\n function updateLoadMoreButton() {\n const loadMoreSection = document.getElementById('load-more-section');\n loadMoreSection.style.display = hasMore && !isLoading ? 'block' : 'none';\n }\n\n // Get filtered sessions based on current filter\n function getFilteredSessions() {\n if (currentSourceFilter === 'all') {\n return allSessions;\n }\n return allSessions.filter(session => session.source === currentSourceFilter);\n }\n\n // Render all sessions (grouped by date)\n function renderAllSessions() {\n const container = document.getElementById('sessions-container');\n container.innerHTML = ''; // Clear existing\n\n const filteredSessions = getFilteredSessions();\n\n if (filteredSessions.length === 0) {\n container.innerHTML = '<div style=\"text-align: center; color: #6e7681; padding: 40px; font-size: 14px;\">No sessions found for this filter.</div>';\n return;\n }\n\n const grouped = groupSessionsByDate(filteredSessions);\n const sortedDates = Object.keys(grouped).sort((a, b) => b.localeCompare(a)); // Descending\n\n sortedDates.forEach(dateKey => {\n const dateHeader = document.createElement('div');\n dateHeader.className = 'date-group-header';\n dateHeader.textContent = formatDateHeader(grouped[dateKey][0].createdAt);\n container.appendChild(dateHeader);\n\n const grid = document.createElement('div');\n grid.className = 'recent-list';\n grouped[dateKey].forEach(session => {\n grid.innerHTML += renderSessionCard(session);\n });\n container.appendChild(grid);\n });\n }\n\n // Check if user has scrolled near bottom\n function checkScrollPosition() {\n const scrollTop = window.pageYOffset || document.documentElement.scrollTop;\n const windowHeight = window.innerHeight;\n const docHeight = document.documentElement.scrollHeight;\n\n // Load more when user is within 500px of bottom\n if (scrollTop + windowHeight >= docHeight - 500 && hasMore && !isLoading) {\n loadMoreSessions();\n }\n }\n\n // Throttle scroll events for performance\n let scrollTimeout;\n function throttledScroll() {\n if (scrollTimeout) return;\n scrollTimeout = setTimeout(() => {\n checkScrollPosition();\n scrollTimeout = null;\n }, 100);\n }\n\n function viewSession(e) {\n e.preventDefault();\n const sessionId = document.getElementById('sessionInput').value.trim();\n if (sessionId) {\n window.location.href = `/session/${sessionId}`;\n }\n }\n\n // File import handling\n const fileInput = document.getElementById('fileInput');\n const importLink = document.getElementById('importLink');\n const importStatus = document.getElementById('importStatus');\n \n // Click import link to select file\n importLink.addEventListener('click', (e) => {\n e.preventDefault();\n fileInput.click();\n });\n \n // Auto-upload when file is selected\n fileInput.addEventListener('change', async (e) => {\n const file = e.target.files[0];\n if (!file) return;\n \n if (!file.name.endsWith('.zip')) {\n showStatus('error', '❌ Please select a .zip file');\n return;\n }\n \n importLink.style.pointerEvents = 'none';\n importLink.style.opacity = '0.5';\n importLink.textContent = 'Importing...';\n showStatus('loading', 'Uploading and extracting session...');\n \n try {\n const formData = new FormData();\n formData.append('sessionZip', file);\n \n const response = await fetch('/session/import', {\n method: 'POST',\n body: formData\n });\n \n const result = await response.json();\n \n if (response.ok) {\n showStatus('success', `✅ Session ${result.sessionId} imported successfully!`);\n \n // Reload page after 1.5 seconds to show new session\n setTimeout(() => {\n window.location.reload();\n }, 1500);\n } else {\n showStatus('error', `❌ Import failed: ${result.error}`);\n importLink.style.pointerEvents = 'auto';\n importLink.style.opacity = '1';\n importLink.textContent = 'Import session from zip';\n }\n } catch (err) {\n showStatus('error', `❌ Import failed: ${err.message}`);\n importLink.style.pointerEvents = 'auto';\n importLink.style.opacity = '1';\n importLink.textContent = 'Import session from zip';\n } finally {\n // Reset file input\n fileInput.value = '';\n }\n });\n \n function showStatus(type, message) {\n importStatus.className = `import-status ${type}`;\n importStatus.textContent = message;\n }\n\n // Format duration from milliseconds to human-readable format\n function formatDuration(ms) {\n if (!ms || ms < 0) return '—';\n \n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n \n if (hours > 0) {\n const remainingMinutes = minutes % 60;\n return `${hours}h ${remainingMinutes}m`;\n } else if (minutes > 0) {\n const remainingSeconds = seconds % 60;\n return `${minutes}m ${remainingSeconds}s`;\n } else {\n return `${seconds}s`;\n }\n }\n \n // Format date to YYYY/MM/DD\n function formatDateHeader(dateStr) {\n const date = new Date(dateStr);\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n return `${year}/${month}/${day}`;\n }\n \n // Get date key from timestamp (YYYY-MM-DD for grouping)\n function getDateKey(timestamp) {\n if (!timestamp) return 'Unknown';\n const date = new Date(timestamp);\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n return `${year}-${month}-${day}`;\n }\n \n // Group sessions by date\n function groupSessionsByDate(sessions) {\n const groups = {};\n sessions.forEach(session => {\n const dateKey = getDateKey(session.createdAt);\n if (!groups[dateKey]) {\n groups[dateKey] = [];\n }\n groups[dateKey].push(session);\n });\n return groups;\n }\n \n // Render session card HTML\n function renderSessionCard(session) {\n // Add status badges\n let badges = '';\n\n // Add source badge (at the beginning)\n const sourceClass = session.source === 'claude' ? 'source-claude' : 'source-copilot';\n const sourceLabel = session.source === 'claude' ? 'Claude' : 'Copilot';\n badges += `<span class=\"status-badge ${sourceClass}\" title=\"${sourceLabel} CLI\">${sourceLabel}</span>`;\n\n if (session.sessionStatus === 'wip') {\n badges += '<span class=\"status-badge wip\" title=\"Session in progress\">🔄 WIP</span>';\n }\n if (session.isImported) {\n badges += '<span class=\"status-badge imported\" title=\"Imported session\">📥</span>';\n }\n if (session.hasInsight) {\n badges += '<span class=\"status-badge insight\" title=\"Has Copilot Insight\">💡</span>';\n }\n // Add model and version badges\n if (session.selectedModel) {\n const modelShort = session.selectedModel.replace('claude-', '').replace('gpt-', '').replace('gemini-', '');\n let modelClass = 'model-other';\n if (session.selectedModel.includes('claude')) {\n modelClass = 'model-claude';\n } else if (session.selectedModel.includes('gpt')) {\n modelClass = 'model-gpt';\n } else if (session.selectedModel.includes('gemini')) {\n modelClass = 'model-gemini';\n }\n badges += `<span class=\"status-badge model ${modelClass}\" title=\"Model: ${escapeHtml(session.selectedModel)}\">${escapeHtml(modelShort)}</span>`;\n }\n if (session.copilotVersion) {\n badges += `<span class=\"status-badge version\" title=\"Copilot CLI version\">${escapeHtml(session.copilotVersion)}</span>`;\n }\n \n let summaryHtml = '';\n if (session.summary && session.summary !== 'No summary' && session.summary !== 'Legacy session') {\n summaryHtml = `<div class=\"session-summary\">${escapeHtml(session.summary)}</div>`;\n } else {\n summaryHtml = '<div class=\"session-summary\" style=\"color: #6e7681; font-style: italic;\">No summary available</div>';\n }\n \n let workspaceHtml = '';\n if (session.workspace && session.workspace.cwd) {\n workspaceHtml = `\n <div class=\"session-info-item workspace\" title=\"${escapeHtml(session.workspace.cwd)}\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M1.75 1A1.75 1.75 0 000 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25v-8.5A1.75 1.75 0 0014.25 3H7.5a.25.25 0 01-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75z\"></path></svg>\n <span class=\"session-info-value\">${escapeHtml(session.workspace.cwd)}</span>\n </div>\n `;\n }\n \n const createdAtStr = session.createdAt \n ? new Date(session.createdAt).toLocaleString('en-US', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })\n : 'unknown';\n \n let durationHtml = '';\n if (session.duration) {\n durationHtml = `\n <div class=\"session-info-item\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM7.25 12.5v-5A.75.75 0 0 1 8 6.75h2.5a.75.75 0 0 1 0 1.5H8.75v4.25a.75.75 0 0 1-1.5 0Z\"></path></svg>\n <span class=\"session-info-value\">${formatDuration(session.duration)}</span>\n </div>\n `;\n }\n \n const wipClass = session.sessionStatus === 'wip' ? ' recent-item-wip' : '';\n\n return `\n <a href=\"/session/${session.id}\" class=\"recent-item${wipClass}\">\n <div class=\"session-id\">\n <span class=\"session-id-text\" title=\"${escapeHtml(session.id)}\">${escapeHtml(session.id)}</span>\n <div class=\"session-badges\">${badges}</div>\n </div>\n ${summaryHtml}\n <div class=\"session-divider\"></div>\n <div class=\"session-info\">\n ${workspaceHtml}\n <div class=\"session-info-item\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.751.751 0 0 1 7 8.25v-3.5a.75.75 0 0 1 1.5 0Z\"></path></svg>\n <span class=\"session-info-value\">${createdAtStr}</span>\n </div>\n ${durationHtml}\n <div class=\"session-info-item\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M7.72.72a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-.22-.22v1.69a.75.75 0 0 1-1.5 0V3.06l-.22.22a.75.75 0 0 1-1.06-1.06ZM2 7a.75.75 0 0 0 0 1.5h3.69l-.22.22a.75.75 0 1 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06l-1.5-1.5a.75.75 0 0 0-1.06 1.06l.22.22Zm8.53-.28a.75.75 0 0 0 0 1.06l1.5 1.5a.75.75 0 1 0 1.06-1.06l-.22-.22H16a.75.75 0 0 0 0-1.5h-3.13l.22-.22a.75.75 0 0 0-1.06-1.06ZM7.72 12.22a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 1 1-1.06 1.06l-.22-.22v1.69a.75.75 0 0 1-1.5 0v-1.69l-.22.22a.75.75 0 0 1-1.06-1.06Z\"></path></svg>\n <span class=\"session-info-value\">${session.eventCount || 0} events</span>\n </div>\n </div>\n </a>\n `;\n }\n \n function escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n // Filter pill click handler\n function setupFilterPills() {\n const filterPills = document.querySelectorAll('.filter-pill');\n filterPills.forEach(pill => {\n pill.addEventListener('click', () => {\n // Remove active class from all pills\n filterPills.forEach(p => p.classList.remove('active'));\n // Add active class to clicked pill\n pill.classList.add('active');\n\n // Update filter state\n currentSourceFilter = pill.getAttribute('data-source');\n\n // Re-render sessions with filter\n renderAllSessions();\n });\n });\n }\n\n // Render grouped sessions\n document.addEventListener('DOMContentLoaded', function() {\n // Initial render\n renderAllSessions();\n updateLoadMoreButton();\n\n // Add scroll listener for infinite scroll\n window.addEventListener('scroll', throttledScroll);\n\n // Add click listener for load more button\n document.getElementById('load-more-btn').addEventListener('click', loadMoreSessions);\n\n // Setup filter pills\n setupFilterPills();\n });\n ",
6
+ "functions": [
7
+ {
8
+ "functionName": "",
9
+ "isBlockCoverage": true,
10
+ "ranges": [
11
+ {
12
+ "startOffset": 0,
13
+ "endOffset": 14748,
14
+ "count": 1
15
+ }
16
+ ]
17
+ },
18
+ {
19
+ "functionName": "loadMoreSessions",
20
+ "isBlockCoverage": false,
21
+ "ranges": [
22
+ {
23
+ "startOffset": 607,
24
+ "endOffset": 1828,
25
+ "count": 0
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "functionName": "updateLoadMoreButton",
31
+ "isBlockCoverage": true,
32
+ "ranges": [
33
+ {
34
+ "startOffset": 1876,
35
+ "endOffset": 2071,
36
+ "count": 1
37
+ },
38
+ {
39
+ "startOffset": 2056,
40
+ "endOffset": 2064,
41
+ "count": 0
42
+ }
43
+ ]
44
+ },
45
+ {
46
+ "functionName": "getFilteredSessions",
47
+ "isBlockCoverage": true,
48
+ "ranges": [
49
+ {
50
+ "startOffset": 2130,
51
+ "endOffset": 2331,
52
+ "count": 1
53
+ },
54
+ {
55
+ "startOffset": 2241,
56
+ "endOffset": 2330,
57
+ "count": 0
58
+ }
59
+ ]
60
+ },
61
+ {
62
+ "functionName": "",
63
+ "isBlockCoverage": false,
64
+ "ranges": [
65
+ {
66
+ "startOffset": 2274,
67
+ "endOffset": 2323,
68
+ "count": 0
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ "functionName": "renderAllSessions",
74
+ "isBlockCoverage": true,
75
+ "ranges": [
76
+ {
77
+ "startOffset": 2382,
78
+ "endOffset": 3505,
79
+ "count": 1
80
+ },
81
+ {
82
+ "startOffset": 2631,
83
+ "endOffset": 2811,
84
+ "count": 0
85
+ }
86
+ ]
87
+ },
88
+ {
89
+ "functionName": "",
90
+ "isBlockCoverage": true,
91
+ "ranges": [
92
+ {
93
+ "startOffset": 2926,
94
+ "endOffset": 2954,
95
+ "count": 2
96
+ }
97
+ ]
98
+ },
99
+ {
100
+ "functionName": "",
101
+ "isBlockCoverage": true,
102
+ "ranges": [
103
+ {
104
+ "startOffset": 2998,
105
+ "endOffset": 3497,
106
+ "count": 3
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ "functionName": "",
112
+ "isBlockCoverage": true,
113
+ "ranges": [
114
+ {
115
+ "startOffset": 3372,
116
+ "endOffset": 3450,
117
+ "count": 20
118
+ }
119
+ ]
120
+ },
121
+ {
122
+ "functionName": "checkScrollPosition",
123
+ "isBlockCoverage": false,
124
+ "ranges": [
125
+ {
126
+ "startOffset": 3557,
127
+ "endOffset": 3961,
128
+ "count": 0
129
+ }
130
+ ]
131
+ },
132
+ {
133
+ "functionName": "throttledScroll",
134
+ "isBlockCoverage": false,
135
+ "ranges": [
136
+ {
137
+ "startOffset": 4036,
138
+ "endOffset": 4220,
139
+ "count": 0
140
+ }
141
+ ]
142
+ },
143
+ {
144
+ "functionName": "viewSession",
145
+ "isBlockCoverage": false,
146
+ "ranges": [
147
+ {
148
+ "startOffset": 4226,
149
+ "endOffset": 4448,
150
+ "count": 0
151
+ }
152
+ ]
153
+ },
154
+ {
155
+ "functionName": "",
156
+ "isBlockCoverage": false,
157
+ "ranges": [
158
+ {
159
+ "startOffset": 4752,
160
+ "endOffset": 4817,
161
+ "count": 0
162
+ }
163
+ ]
164
+ },
165
+ {
166
+ "functionName": "",
167
+ "isBlockCoverage": false,
168
+ "ranges": [
169
+ {
170
+ "startOffset": 4907,
171
+ "endOffset": 6475,
172
+ "count": 0
173
+ }
174
+ ]
175
+ },
176
+ {
177
+ "functionName": "showStatus",
178
+ "isBlockCoverage": false,
179
+ "ranges": [
180
+ {
181
+ "startOffset": 6487,
182
+ "endOffset": 6627,
183
+ "count": 0
184
+ }
185
+ ]
186
+ },
187
+ {
188
+ "functionName": "formatDuration",
189
+ "isBlockCoverage": true,
190
+ "ranges": [
191
+ {
192
+ "startOffset": 6699,
193
+ "endOffset": 7226,
194
+ "count": 4
195
+ },
196
+ {
197
+ "startOffset": 6754,
198
+ "endOffset": 6765,
199
+ "count": 0
200
+ },
201
+ {
202
+ "startOffset": 6940,
203
+ "endOffset": 7045,
204
+ "count": 0
205
+ },
206
+ {
207
+ "startOffset": 7068,
208
+ "endOffset": 7220,
209
+ "count": 2
210
+ }
211
+ ]
212
+ },
213
+ {
214
+ "functionName": "formatDateHeader",
215
+ "isBlockCoverage": true,
216
+ "ranges": [
217
+ {
218
+ "startOffset": 7269,
219
+ "endOffset": 7553,
220
+ "count": 3
221
+ }
222
+ ]
223
+ },
224
+ {
225
+ "functionName": "getDateKey",
226
+ "isBlockCoverage": true,
227
+ "ranges": [
228
+ {
229
+ "startOffset": 7624,
230
+ "endOffset": 7946,
231
+ "count": 20
232
+ },
233
+ {
234
+ "startOffset": 7679,
235
+ "endOffset": 7696,
236
+ "count": 0
237
+ }
238
+ ]
239
+ },
240
+ {
241
+ "functionName": "groupSessionsByDate",
242
+ "isBlockCoverage": true,
243
+ "ranges": [
244
+ {
245
+ "startOffset": 7986,
246
+ "endOffset": 8292,
247
+ "count": 1
248
+ }
249
+ ]
250
+ },
251
+ {
252
+ "functionName": "",
253
+ "isBlockCoverage": true,
254
+ "ranges": [
255
+ {
256
+ "startOffset": 8075,
257
+ "endOffset": 8263,
258
+ "count": 20
259
+ },
260
+ {
261
+ "startOffset": 8173,
262
+ "endOffset": 8216,
263
+ "count": 3
264
+ }
265
+ ]
266
+ },
267
+ {
268
+ "functionName": "renderSessionCard",
269
+ "isBlockCoverage": true,
270
+ "ranges": [
271
+ {
272
+ "startOffset": 8334,
273
+ "endOffset": 13482,
274
+ "count": 20
275
+ },
276
+ {
277
+ "startOffset": 8522,
278
+ "endOffset": 8539,
279
+ "count": 16
280
+ },
281
+ {
282
+ "startOffset": 8540,
283
+ "endOffset": 8558,
284
+ "count": 4
285
+ },
286
+ {
287
+ "startOffset": 8614,
288
+ "endOffset": 8624,
289
+ "count": 16
290
+ },
291
+ {
292
+ "startOffset": 8625,
293
+ "endOffset": 8636,
294
+ "count": 4
295
+ },
296
+ {
297
+ "startOffset": 8792,
298
+ "endOffset": 8895,
299
+ "count": 0
300
+ },
301
+ {
302
+ "startOffset": 8926,
303
+ "endOffset": 9027,
304
+ "count": 0
305
+ },
306
+ {
307
+ "startOffset": 9058,
308
+ "endOffset": 9161,
309
+ "count": 0
310
+ },
311
+ {
312
+ "startOffset": 9233,
313
+ "endOffset": 9854,
314
+ "count": 14
315
+ },
316
+ {
317
+ "startOffset": 9445,
318
+ "endOffset": 9495,
319
+ "count": 9
320
+ },
321
+ {
322
+ "startOffset": 9495,
323
+ "endOffset": 9693,
324
+ "count": 5
325
+ },
326
+ {
327
+ "startOffset": 9544,
328
+ "endOffset": 9591,
329
+ "count": 0
330
+ },
331
+ {
332
+ "startOffset": 9643,
333
+ "endOffset": 9693,
334
+ "count": 0
335
+ },
336
+ {
337
+ "startOffset": 9889,
338
+ "endOffset": 10027,
339
+ "count": 16
340
+ },
341
+ {
342
+ "startOffset": 10166,
343
+ "endOffset": 10266,
344
+ "count": 19
345
+ },
346
+ {
347
+ "startOffset": 10266,
348
+ "endOffset": 10406,
349
+ "count": 1
350
+ },
351
+ {
352
+ "startOffset": 10498,
353
+ "endOffset": 10998,
354
+ "count": 16
355
+ },
356
+ {
357
+ "startOffset": 11211,
358
+ "endOffset": 11222,
359
+ "count": 0
360
+ },
361
+ {
362
+ "startOffset": 11288,
363
+ "endOffset": 11727,
364
+ "count": 4
365
+ },
366
+ {
367
+ "startOffset": 11790,
368
+ "endOffset": 11810,
369
+ "count": 0
370
+ },
371
+ {
372
+ "startOffset": 13399,
373
+ "endOffset": 13403,
374
+ "count": 0
375
+ }
376
+ ]
377
+ },
378
+ {
379
+ "functionName": "escapeHtml",
380
+ "isBlockCoverage": true,
381
+ "ranges": [
382
+ {
383
+ "startOffset": 13492,
384
+ "endOffset": 13632,
385
+ "count": 135
386
+ }
387
+ ]
388
+ },
389
+ {
390
+ "functionName": "setupFilterPills",
391
+ "isBlockCoverage": true,
392
+ "ranges": [
393
+ {
394
+ "startOffset": 13671,
395
+ "endOffset": 14256,
396
+ "count": 1
397
+ }
398
+ ]
399
+ },
400
+ {
401
+ "functionName": "",
402
+ "isBlockCoverage": true,
403
+ "ranges": [
404
+ {
405
+ "startOffset": 13796,
406
+ "endOffset": 14248,
407
+ "count": 3
408
+ }
409
+ ]
410
+ },
411
+ {
412
+ "functionName": "",
413
+ "isBlockCoverage": false,
414
+ "ranges": [
415
+ {
416
+ "startOffset": 13845,
417
+ "endOffset": 14238,
418
+ "count": 0
419
+ }
420
+ ]
421
+ },
422
+ {
423
+ "functionName": "",
424
+ "isBlockCoverage": true,
425
+ "ranges": [
426
+ {
427
+ "startOffset": 14339,
428
+ "endOffset": 14743,
429
+ "count": 1
430
+ }
431
+ ]
432
+ }
433
+ ]
434
+ }
435
+ ]