1import { changeRequestsApi } from '../api/change-requests.js'; 2import { offlineApi } from '../offline/offline-api.js'; 3import { renderTo, loadTemplates, createTemplateLoader } from '../utils/templates.js'; 4import { runWithBusyButton } from '../utils/dom.js'; 5import { downloadBlob } from '../utils/download.js'; 6import { attachDropdownMenu } from '../utils/dropdown-menu.js'; 7import { showLogEmailCompose } from '../utils/email-compose.js'; 8import { createDesignerTaskDetail } from './designer-task-detail.js'; 9import { updateFilterPills as _updateFilterPills } from '../utils/filter-pills.js'; 10import { openChgCreateModal } from '../utils/chg-create-modal.js'; 11import { DataView } from '../components/DataView.js'; 12import './chg-task-modals.js'; // side-effect: registers CHG_IDN / CHG_STS / CHG_QR / CHG_QTO / CHG_OA / CHG_COI / ADD2PROAPP / CHG_REJECT with the task-modal factory 13import { openTaskModal } from '../utils/task-modal-factory.js'; 14 15import { subscribeTaskUpdates } from '../realtime/TaskGroupSync.js'; 16 17// Page link values that map to the "chg" sub-module tab 18export const CHG_PAGE_LINKS = ['chg']; 19 20let currentProjectId = null; 21let items = []; 22let filteredItems = []; 23let signal = null; 24let dialogAbort = null; 25const ensureTemplates = createTemplateLoader('components/chg-templates'); 26let activeStatusFilter = 'all'; 27let onNavigateCallback = null; 28 29// CCM data from API 30let listData = null; 31 32// Shared detail view instance (created once, reused) 33let detailView = null; 34let currentContainerId = 'project-chg-container'; 35 36// DataView (table/cards toggle) â recreated each time the list shell renders 37let dataView = null; 38 39// Shared click-to-sort on the table headers (DataView.initSortableHeaders) â 40// recreated alongside dataView; th[data-sort-key] attrs live in the template. 41let sorter = null; 42 43// SignalR live list refresh â subscribed to chglist-{projectId} while the list 44// is loaded; disposed on destroy/project switch (re-subscribed by loadItems). 45let listSync = null; 46 47// Export dropdown handle â set by attachEventHandlers after each list render, 48// used by the page-level export click handler below. 49let exportDropdown = null; 50 51// Own AbortController for the export dropdown's listeners (see attachEventHandlers 52// below) â NOT the page `signal`. attachEventHandlers() re-runs on every list 53// re-render (loadItems, every "back to list" from detail), and attachDropdownMenu 54// binds its outside-click/Escape listeners on `document` â the most persistent 55// target there is. The page signal only aborts on full contract-admin teardown, 56// not on list re-renders, so reusing it here would stack a fresh pair of 57// document-level listeners on every render, forever (each closing over that 58// render's now-detached menu/button â harmless no-ops, but never removed). 59// Same fix as project-rfi.js exportDropdownAbort. 60let exportDropdownAbort = null; 61 62// "Show Deleted" toggle (legacy parity) â includes soft-deleted rows in the 63// list. Per-visit only: reset on destroy/project switch, never persisted. 64let showDeleted = false; 65 66/** 67 * Initialize CHG sub-module. 68 * Called when the CHG tab is selected in Projects. 69 * @param {number} projectId - The selected project ID 70 * @param {AbortSignal} [abortSignal] - AbortController signal for cleanup 71 * @param {Object} [options] - Optional config 72 * @param {string} [options.initialTaskId] - Task ID to deep-link into detail view 73 * @param {Function} [options.onNavigate] - Callback when view changes (for URL updates) 74 */ 75export async function initProjectChg(projectId, abortSignal, options = {}) { 76 dialogAbort?.abort(); 77 detailView?.destroy(); 78 const visitController = new AbortController(); 79 dialogAbort = visitController; 80 if (abortSignal?.aborted) visitController.abort(); 81 else abortSignal?.addEventListener('abort', () => visitController.abort(), { once: true, signal: visitController.signal }); 82 if (visitController.signal.aborted) return; 83 signal = abortSignal || null; 84 onNavigateCallback = options.onNavigate || null; 85 const containerId = options.containerId || 'project-chg-container'; 86 87 await ensureTemplates();
88 if (dialogAbort !== visitController || visitController.signal.aborted) return; 89 90 // Recreate detail view if container changed 91 if (currentContainerId !== containerId && detailView) { 92 detailView.destroy(); 93 detailView = null; 94 } 95 currentContainerId = containerId; 96 97 // Create detail view instance if not exists 98 if (!detailView) { 99 detailView = createDesignerTaskDetail({ 100 api: changeRequestsApi, 101 containerId: currentContainerId, 102 entityLabel: 'CHG', 103 entityIcon: 'git-pull-request', 104 onSpecialAction: handleSpecialAction, 105 itemType: 'CHG', 106 // CA-017 opt-in: issue-package milestone codes (modern + legacy aliases) â 107 // renders the truthful Not sent / Queued / Failed / Sent strip + retry. 108 deliveryStateCodes: ['CHG_STS', 'CISSPK', 'S2SUB'], 109 // Reject dispatcher (audit R4, guard fixed CA-056): the CHG_REJECT modal + 110 // endpoint existed with no way to reach them. Header action (legacy put 111 // "Cancel" in the REAPP row kebab; a header button is the canon home). 112 // Reject is a header action with NO task date, so it dispatches with 113 // taskDateId = null â handleSpecialAction's taskDateId guard is scoped to 114 // the date-type actions only (see there). Server enforces the 115 // rejected-state lock, so re-clicking on a rejected CHG is a harmless re-stamp. 116 extraActions: [{ 117 id: 'chg-reject', 118 label: 'Reject', 119 icon: 'ban', 120 onClick: ({ taskId }) => handleSpecialAction('chg-reject', null, null, taskId), 121 }], 122 }); 123 } 124 125 // If project changed, clear cached data and reset detail view 126 if (currentProjectId !== projectId) { 127 items = []; 128 filteredItems = []; 129 listData = null; 130 activeStatusFilter = 'all'; 131 showDeleted = false; 132 detailView.destroy(); 133 } 134 currentProjectId = projectId; 135 136 attachPageLevelHandlers(); 137 138 await loadItems(); 139 if (dialogAbort !== visitController || visitController.signal.aborted) return; 140 141 // Deep-link into detail view if task ID provided via URL 142 if (options.initialTaskId) { 143 await detailView.loadDetail(currentProjectId, options.initialTaskId, signal, { 144 onNavigate: onNavigateCallback, 145 onBackToList: async () => { await renderList(); attachEventHandlers(); }, 146 onReloadList: () => loadItems(), 147 }); 148 } 149} 150 151/** 152 * Clean up CHG state. 153 */ 154export function destroyProjectChg() { 155 dialogAbort?.abort(); 156 dialogAbort = null; 157 detailView?.destroy(); 158 detailView = null; 159 dataView?.destroy(); 160 dataView = null; 161 sorter?.destroy(); 162 sorter = null; 163 listSync?.dispose(); 164 listSync = null; 165 currentProjectId = null; 166 items = []; 167 filteredItems = []; 168 listData = null; 169 signal = null; 170 activeStatusFilter = 'all'; 171 showDeleted = false; 172 onNavigateCallback = null; 173 currentContainerId = 'project-chg-container'; 174 exportDropdownAbort?.abort(); 175 exportDropdownAbort = null; 176 exportDropdown = null; 177} 178 179/** 180 * Get the currently viewed CHG task ID (for URL building). 181 */ 182export function getCurrentChgTaskId() { 183 return detailView?.getCurrentTaskId() || null; 184} 185 186// ======================================== 187// DATA LOADING 188// ======================================== 189 190/** 191 * Silent list refresh for SignalR pings â refetch + re-filter without 192 * tearing down the list shell (no loading skeleton, no handler rebinds). 193 */ 194async function refreshListData() { 195 if (!currentProjectId) return; 196 const pid = currentProjectId; 197 try { 198 const response = await changeRequestsApi.getList(pid, null, showDeleted); 199 const serverRows = await offlineApi.prioritizeOfflineAvailable(response.rfis || [], { 200 feature: 'change-requests', 201 idGetter: item => item.crId ?? item.rfiId ?? item.taskId, 202 }); 203 if (pid !== currentProjectId) return; // project switched mid-flight â drop stale rows 204 listData = response; 205 items = serverRows; 206 const searchInput = document.getElementById('chg-search'); 207 applyFilters(searchInput?.value || ''); 208 } catch { /* silent â SignalR will ping again on the next change */ } 209} 210 211async function loadItems(isCurrent = null) { 212 if (isCurrent && !isCurrent()) return; 213 const projectId = currentProjectId; 214 const readSignal = isCurrent ? dialogAbort?.signal : undefined; 215 const container = document.getElementById(currentContainerId); 216 if (!container || !currentProjectId) return; 217 218 // Live list refresh: anyone's create/edit/delete/date/comment on this project's 219 // Change Requests pings chglist-{projectId} over SignalR (the app's ONE transport â 220 // no client polling, ever) â silent row refresh. 221 listSync?.dispose(); 222 listSync = subscribeTaskUpdates(`chglist-${currentProjectId}`, () => { refreshListData(); }); 223 224 await renderChgOwned(container, '#tpl-chg-loading', {}, isCurrent); 225 if (isCurrent && !isCurrent()) return; 226 227 try { 228 const response = await changeRequestsApi.getList(projectId, null, showDeleted, readSignal ? { signal: readSignal }
228 : undefined); 229 if (isCurrent && !isCurrent()) return; 230 const availableItems = await offlineApi.prioritizeOfflineAvailable(response.rfis || [], { 231 feature: 'change-requests', 232 idGetter: item => item.crId ?? item.rfiId ?? item.taskId, 233 }); 234 if (isCurrent && !isCurrent()) return; 235 listData = response; 236 items = availableItems; 237 filteredItems = [...items]; 238 239 await renderList(isCurrent); 240 if (isCurrent && !isCurrent()) return; 241 attachEventHandlers(); 242 } catch (error) { 243 if ((isCurrent && !isCurrent()) || error.name === 'AbortError') return; 244 window.showToast?.('Failed to load Change Requests', 'error', { description: error.message }); 245 } 246} 247 248// ======================================== 249// RENDERING 250// ======================================== 251 252async function renderChgOwned(container, templateId, data, isCurrent) { 253 if (!isCurrent) return renderTo(container, templateId, data); 254 if (!isCurrent()) return; 255 const staging = document.createElement('div'); 256 try { await renderTo(staging, templateId, data); } 257 catch (error) { if (!isCurrent()) return; throw error; } 258 if (isCurrent()) container.replaceChildren(...staging.childNodes); 259} 260 261async function renderList(isCurrent = null) { 262 if (isCurrent && !isCurrent()) return; 263 const container = document.getElementById(currentContainerId); 264 if (!container) return; 265 266 await renderChgOwned(container, '#tpl-chg-list', { 267 totalCount: items.length, 268 overdueCount: listData?.overdueCount || 0, 269 dueTodayCount: listData?.dueTodayCount || 0, 270 upcomingCount: listData?.upcomingCount || 0, 271 closedCount: listData?.closedCount || 0, 272 showDeleted, 273 }, isCurrent); 274 if (isCurrent && !isCurrent()) return; 275 276 // Wire DataView for the table/cards toggle (must happen AFTER the shell 277 // renders so the table-view / cards-view / toggle containers exist). 278 dataView?.destroy(); 279 dataView = DataView.init({ 280 storageKey: 'chg', 281 tableContainerId: 'chg-table-view', 282 cardsContainerId: 'chg-cards-view', 283 toggleContainerId: 'chg-view-toggle', 284 defaultView: 'table', 285 onViewChange: () => renderCurrentView(isCurrent), 286 }); 287 288 sorter?.destroy(); 289 sorter = DataView.initSortableHeaders({ 290 storageKey: 'chg', 291 tableContainerId: 'chg-table-view', 292 onSort: () => renderCurrentView(isCurrent), 293 }); 294 295 await renderCurrentView(isCurrent); 296 if (isCurrent && !isCurrent()) return; 297 298 // Restore active filter pill state 299 updateFilterPills(); 300} 301 302/** 303 * Render the active view (table or cards) from filteredItems. 304 */ 305async function renderCurrentView(isCurrent = null) { 306 if (isCurrent && !isCurrent()) return; 307 const tableBody = document.getElementById('chg-table-body'); 308 const cardsContainer = document.getElementById('chg-items'); 309 310 if (filteredItems.length === 0) { 311 if (tableBody) await renderChgOwned(tableBody, '#tpl-chg-table-empty', {}, isCurrent); 312 if (cardsContainer) await renderChgOwned(cardsContainer, '#tpl-chg-empty', {}, isCurrent); 313 return; 314 } 315 316 // Header sort applies to BOTH views so toggling tableâcards keeps the order 317 const rows = sorter ? sorter.sortItems(filteredItems) : filteredItems; 318 const view = dataView?.getView() ?? 'table'; 319 if (view === 'table' && tableBody) { 320 await renderChgOwned(tableBody, '#tpl-chg-table-row', rows, isCurrent); 321 } else if (cardsContainer) { 322 await renderChgOwned(cardsContainer, '#tpl-chg-row', rows, isCurrent); 323 } 324} 325 326function updateFilterPills() { 327 _updateFilterPills('.chg-filter-pill', activeStatusFilter); 328} 329 330function attachEventHandlers() { 331 const container = document.getElementById(currentContainerId); 332 if (!container) return; 333 334 const opts = signal ? { signal } : {}; 335 336 // Search 337 const searchInput = document.getElementById('chg-search'); 338 searchInput?.addEventListener('input', (e) => { 339 applyFilters(e.target.value); 340 }, opts); 341 342 // Status filter pills 343 const filterContainer = document.getElementById('chg-status-filters'); 344 filterContainer?.addEventListener('click', (e) => { 345 const pill = e.target.closest('.chg-filter-pill'); 346 if (!pill) return;
347 activeStatusFilter = pill.dataset.filter; 348 const searchInput = document.getElementById('chg-search'); 349 applyFilters(searchInput?.value || ''); 350 updateFilterPills(); 351 }, opts); 352 353 // "Show Deleted" toggle â server-side param, so flip â full reload. The 354 // shell re-render restores the checked state from module-level `showDeleted`. 355 const showDeletedToggle = document.getElementById('chg-show-deleted'); 356 showDeletedToggle?.addEventListener('change', () => { 357 showDeleted = showDeletedToggle.checked; 358 loadItems(); 359 }, opts); 360 361 // ââ Export dropdown toggle (button is inside the re-rendered list shell) ââ 362 // Abort the PREVIOUS render's dropdown listeners before attaching a fresh pair 363 // (see exportDropdownAbort declaration above) â otherwise document click/keydown 364 // listeners stack forever across list<->detail round trips. 365 exportDropdownAbort?.abort(); 366 exportDropdownAbort = new AbortController(); 367 exportDropdown = attachDropdownMenu('btn-chg-export', 'chg-export-menu', { signal: exportDropdownAbort.signal }); 368} 369 370// Bound once per page init (not per list render) â these delegate on the 371// persistent outer container element and must not stack across re-renders. 372// Same hazard + same cure as project-rfi.js attachPageLevelHandlers: the page 373// AbortSignal only aborts on full page teardown, not on sub-module/project 374// switch, so re-binding on every render STACKS listeners (one click on "New" 375// was opening 4-5 identical create modals). Handlers read module-level state 376// (currentProjectId, detailView, signal, â¦) at fire time, so bind-once is safe. 377function attachPageLevelHandlers() { 378 const container = document.getElementById(currentContainerId); 379 if (!container) return; 380 381 if (container.dataset.chgPageHandlersBound === 'true') return; 382 container.dataset.chgPageHandlersBound = 'true'; 383 384 const opts = signal ? { signal } : {}; 385 386 // New Change Request button (delegated â works from header button AND empty state CTA) 387 container.addEventListener('click', (e) => { 388 if (e.target.closest('[data-action="new-chg"]')) { 389 showAddModal(); 390 } 391 }, opts); 392 393 // "Send To" button (delegated â emails the Change Log; legacy Send To) 394 container.addEventListener('click', (e) => { 395 if (e.target.closest('[data-action="send-chg-log"]')) { 396 showSendLogModal(); 397 } 398 }, opts); 399 400 // ââ Export button clicks (legacy Create Report PDF / XLS) ââ 401 container.addEventListener('click', async (e) => { 402 const btn = e.target.closest('.chg-export-btn'); 403 if (!btn) return; 404 exportDropdown?.close(); 405 406 const format = btn.dataset.format; 407 // Legacy report change-type filters: All (omit) / BOTH (Owner Initiated + 408 // Charge to Client) / OWNI / CHCL / BKCH / SCVI. 409 const changeType = btn.dataset.changeType || null; 410 411 await runWithBusyButton(btn, 'Generating...', async () => { 412 const { blob, fileName } = format === 'pdf' 413 ? await changeRequestsApi.downloadLogPdf(currentProjectId, null, null, changeType) 414 : await changeRequestsApi.downloadLogExcel(currentProjectId, null, null, changeType); 415 416 downloadBlob(blob, fileName, { successMessage: `${format.toUpperCase()} downloaded` }); 417 }, { errorTitle: 'Export failed', errorLogPrefix: '[ProjectChg] Export failed:', resetOnSuccess: true }); 418 }, opts); 419 420 // Row click â navigate to detail view. Delegated from `container` so it 421 // fires for both card rows (inside #chg-items) and table rows (inside 422 // #chg-table-body) â both carry `data-chg-id` on the row element. 423 container.addEventListener('click', (e) => { 424 const row = e.target.closest('[data-chg-id]'); 425 if (!row) return; 426 if (row.dataset.offlineUnavailable === 'true') { 427 window.showToast?.('Not saved for offline', 'info', { 428 description: 'Open this Change Request while online to save it for offline viewing.', 429 }); 430 return; 431 }
432 detailView.loadDetail(currentProjectId, row.dataset.chgId, signal, { 433 onNavigate: onNavigateCallback, 434 onBackToList: async () => { await renderList(); attachEventHandlers(); }, 435 onReloadList: () => loadItems(), 436 }); 437 }, opts); 438} 439 440// ======================================== 441// FILTERING 442// ======================================== 443 444function applyFilters(searchTerm = '') { 445 const term = searchTerm.toLowerCase(); 446 447 filteredItems = items.filter(r => { 448 // Status filter 449 if (activeStatusFilter !== 'all' && r.status !== activeStatusFilter) { 450 return false; 451 } 452 453 // Text search 454 if (term) { 455 const title = (r.title || '').toLowerCase(); 456 const ref = (r.referenceNumber || '').toLowerCase(); 457 const extraRef = (r.extraReferenceNumber || '').toLowerCase(); 458 const assigned = (r.assigned || '').toLowerCase(); 459 const changeType = (r.changeTypeDesc || '').toLowerCase(); 460 return title.includes(term) || ref.includes(term) || extraRef.includes(term) || assigned.includes(term) || changeType.includes(term); 461 } 462 463 return true; 464 }); 465 466 renderFilteredRows(); 467} 468 469async function renderFilteredRows() { 470 await renderCurrentView(); 471 const countEl = document.getElementById('chg-count'); 472 if (countEl) countEl.textContent = filteredItems.length; 473} 474 475// ======================================== 476// SPECIAL ACTION MODALS (CHG Date Types) 477// ======================================== 478 479async function handleSpecialAction(action, taskDateId, dateTypeId, taskId) { 480 const actionController = dialogAbort; 481 const dialogSignal = actionController?.signal; 482 const projectId = currentProjectId; 483 const ownedDetail = detailView; 484 const ownedNavigate = onNavigateCallback; 485 const isCurrentVisit = () => dialogAbort === actionController && !dialogSignal?.aborted 486 && currentProjectId === projectId && detailView === ownedDetail; 487 const isCurrentAction = () => isCurrentVisit() && String(ownedDetail?.getCurrentTaskId()) === String(taskId); 488 const isCurrentCallback = () => isCurrentVisit() 489 && (ownedDetail?.getCurrentTaskId() == null || String(ownedDetail.getCurrentTaskId()) === String(taskId)); 490 if (!projectId || !actionController || !taskId || !isCurrentAction()) return; 491 // Header Reject has no task date (MCF-0143). 492 if (action !== 'chg-reject' && !taskDateId) return; 493 494 try { 495 const onComplete = async () => { 496 if (!isCurrentAction()) return; 497 try { 498 await ownedDetail.loadDetail(projectId, taskId, dialogSignal, { 499 onNavigate: (...args) => { if (isCurrentCallback()) ownedNavigate?.(...args); }, 500 onBackToList: async () => { 501 if (!isCurrentCallback()) return; 502 await renderList(isCurrentCallback); 503 if (isCurrentCallback()) attachEventHandlers(); 504 }, 505 onReloadList: () => { if (isCurrentCallback()) return loadItems(isCurrentCallback); }, 506 }); 507 } catch (error) { 508 if (!isCurrentAction() || error.name === 'AbortError') return; 509 window.showToast?.('Failed to open form', 'error', { description: error.message }); 510 } 511 }; 512 513 // Reject needs neither member nor cost-code options. 514 if (action === 'chg-reject') { 515 await openTaskModal('CHG_REJECT', { projectId, taskId, api: changeRequestsApi, signal: dialogSignal, onComplete }); 516 return; 517 } 518 519 const opts = await changeRequestsApi.getAddOptions(projectId, { signal: dialogSignal }); 520 if (!isCurrentAction()) return; 521 const members = opts.members || []; 522 523 // Fetch cost codes for Add to Budget modal (degrades gracefully if endpoint missing) 524 let costCodes = []; 525 try { costCodes = await changeRequestsApi.getCostCodes?.(projectId, { signal: dialogSignal }) || []; }
526 catch (error) { if (!isCurrentAction() || error.name === 'AbortError') return; } 527 if (!isCurrentAction()) return; 528 529 const commonOpts = { 530 projectId, 531 taskId, 532 taskDateId, 533 dateTypeId, 534 api: changeRequestsApi, 535 members, 536 costCodes, 537 signal: dialogSignal, 538 onComplete, 539 }; 540 541 const codeByAction = { 542 'chg-identified': 'CHG_IDN', 543 'chg-sent-sub': 'CHG_STS', 544 'chg-quote-received': 'CHG_QR', 545 'chg-quote-to-owner': 'CHG_QTO', 546 'chg-owner-approval': 'CHG_OA', 547 'chg-co-issued': 'CHG_COI', 548 'chg-add-to-budget': 'ADD2PROAPP', 549 }; 550 const code = codeByAction[action]; 551 if (code) await openTaskModal(code, commonOpts); 552 } catch (error) { 553 if (!isCurrentAction() || error.name === 'AbortError') return; 554 window.showToast?.('Failed to open form', 'error', { description: error.message }); 555 } 556} 557 558// ======================================== 559// SEND LOG ("Send To" â email the Change Log) 560// ======================================== 561 562/** 563 * Open the shared compose modal to email the Change Log PDF to picked recipients. 564 * Legacy parity: "Send To" (email the change log). Legacy sends the FULL log, 565 * so no filter scoping here â the PDF is generated server-side. 566 * Same pattern as project-rfi.js showSendLogModal. 567 */ 568async function showSendLogModal() { 569 if (!currentProjectId) return; 570 571 const proj = listData?.project || {}; 572 // Legacy subject shape: "MMM dd, yyyy - CHANGE LOG | {ProjectNumber} | {ProjectTitle}". 573 const dateStr = new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }); 574 const parts = [`${dateStr} - CHANGE LOG`]; 575 if (proj.projectNumber) parts.push(String(proj.projectNumber)); 576 if (proj.projectTitle) parts.push(proj.projectTitle); 577 const subject = parts.join(' | '); 578 const subtitle = [proj.projectNumber, proj.projectTitle].filter(Boolean).join(' â '); 579 580 await showLogEmailCompose({ 581 projectId: currentProjectId, 582 entityLabel: 'Change Log', 583 subject, 584 subtitle, 585 sendFn: (payload) => changeRequestsApi.sendLogEmail(currentProjectId, payload), 586 }); 587} 588 589// ======================================== 590// CREATE MODAL 591// ======================================== 592 593async function showAddModal() { 594 if (!currentProjectId) return; 595 await openChgCreateModal({ 596 projectId: currentProjectId, 597 signal, 598 onCreated: async ({ chgId, title }) => { 599 window.showToast?.('Proposed Change created', 'success', { description: title }); 600 await loadItems(); 601 if (chgId && detailView) { 602 detailView.loadDetail(currentProjectId, chgId, signal, { 603 onNavigate: onNavigateCallback, 604 onBackToList: async () => { await renderList(); attachEventHandlers(); }, 605 onReloadList: () => loadItems(), 606 }); 607 } 608 }, 609 }); 610}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.