@qiaolei81/copilot-session-viewer 0.2.0 → 0.2.2

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 (31) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +1 -1
  3. package/src/controllers/sessionController.js +3 -3
  4. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-be-responsive-on-mobile-viewport-1771605454041.json +0 -435
  5. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-display-sessions-if-available-1771605462872.json +0 -435
  6. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-JavaScript-errors-gracefully-1771605463381.json +0 -435
  7. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-handle-session-import-dialog-1771605466264.json +0 -435
  8. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-have-working-infinite-scroll-elements-1771605454038.json +0 -435
  9. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-homepage-with-basic-elements-1771605454001.json +0 -435
  10. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-load-time-analysis-page-1771605464990.json +0 -1236
  11. package/.nyc_output/coverage-core-functionality-spec-js-Core-Functionality-Tests-should-navigate-to-session-detail-page-1771605472595.json +0 -1177
  12. package/.nyc_output/coverage-e2e-merged.json +0 -1
  13. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-display-session-list-1771605453565.json +0 -435
  14. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-load-homepage-successfully-1771605453552.json +0 -435
  15. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-navigate-to-session-detail-on-click-1771605469317.json +0 -1134
  16. package/.nyc_output/coverage-homepage-spec-js-Homepage-should-show-session-metadata-1771605460581.json +0 -435
  17. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-display-Load-More-Sessions-button-when-there-are-more-sessions-1771605468486.json +0 -435
  18. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-handle-API-errors-gracefully-during-infinite-scroll-1771605482161.json +0 -471
  19. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-hide-Load-More-button-when-no-more-sessions-available-1771605478370.json +0 -471
  20. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-load-additional-sessions-when-Load-More-button-is-clicked-1771605475059.json +0 -471
  21. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-preserve-session-list-state-during-navigation-1771605494575.json +0 -1633
  22. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-show-loading-state-when-Load-More-button-is-clicked-1771605475401.json +0 -471
  23. package/.nyc_output/coverage-infinite-scroll-spec-js-Infinite-Scroll-should-trigger-infinite-scroll-when-scrolling-near-bottom-1771605476949.json +0 -471
  24. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-clear-search-filter-1771605508542.json +0 -1255
  25. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-event-list-1771605505572.json +0 -1156
  26. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-display-session-metadata-1771605504552.json +0 -701
  27. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-expand-and-collapse-tool-details-1771605515809.json +0 -1182
  28. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-filter-events-by-search-1771605513421.json +0 -1245
  29. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-load-session-detail-page-1771605494974.json +0 -701
  30. package/.nyc_output/coverage-session-detail-spec-js-Session-Detail-Page-should-toggle-content-visibility-1771605550729.json +0 -1177
  31. package/.nyc_output/coverage-unit.json +0 -21
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.2] - 2026-02-27
9
+
10
+ ### Fixed
11
+ - **ETag Cache Bug** - `session.updated`/`session.created` field name typo (should be `updatedAt`/`createdAt`) caused ETag to always be `md5("sessionId-undefined")`, resulting in permanent 304 Not Modified responses — frontend never saw new events for WIP sessions
12
+
13
+ ## [0.2.1] - 2026-02-25
14
+
15
+ ### Changed
16
+ - `.nyc_output/` and `coverage/` directories now properly ignored in `.gitignore`
17
+
18
+ ### Removed
19
+ - NYC coverage intermediate files (`.nyc_output/`, 1.6 MB) from repository
20
+ - Removed 28 coverage data files that should not be committed
21
+
22
+ ### Docs
23
+ - Translated `lib/parsers/README.md` from Chinese to English for international contributors
24
+
8
25
  ## [0.2.0] - 2026-02-25
9
26
 
10
27
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qiaolei81/copilot-session-viewer",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Web UI for viewing GitHub Copilot CLI session logs",
5
5
  "author": "Lei Qiao <qiaolei81@gmail.com>",
6
6
  "license": "MIT",
@@ -182,8 +182,8 @@ class SessionController {
182
182
  // Generate ETag from session ID + timestamp + pagination params (if used)
183
183
  const crypto = require('crypto');
184
184
  const etagBase = isPaginationRequested
185
- ? `${sessionId}-${session.updated || session.created}-${limit}-${offset}`
186
- : `${sessionId}-${session.updated || session.created}`;
185
+ ? `${sessionId}-${session.updatedAt || session.createdAt}-${limit}-${offset}`
186
+ : `${sessionId}-${session.updatedAt || session.createdAt}`;
187
187
  const etag = crypto.createHash('md5').update(etagBase).digest('hex');
188
188
 
189
189
  // Check If-None-Match header (client cache)
@@ -248,7 +248,7 @@ class SessionController {
248
248
 
249
249
  // Set caching headers
250
250
  const crypto = require('crypto');
251
- const etagBase = `${sessionId}-timeline-${session.updated || session.created}`;
251
+ const etagBase = `${sessionId}-timeline-${session.updatedAt || session.createdAt}`;
252
252
  const etag = crypto.createHash('md5').update(etagBase).digest('hex');
253
253
 
254
254
  res.set({
@@ -1,435 +0,0 @@
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
- ]