1// History Manager - LocalStorage management for BiomPIN access history 2import './biompin-sdk.js'; 3 4const BiomPinSDK = window.BiomPinSDK; 5 6// Initialize the stateful history store using BiomPinSDK 7const sdkHistory = BiomPinSDK.history.create({ 8 storageKey: 'biompin_history', 9 maxEntries: 250 10}); 11 12const INITIAL_VISIBLE_COUNT = 5; // Show first 5 items by default 13 14// Track if history is expanded 15let isHistoryExpanded = false; 16 17// Current database ID (persists across deploys, changes only on DB reset) 18let currentDbId = null; 19 20/** 21 * Set the current database ID 22 * @param {string} id - Database UUID from server 23 */ 24export function setDbId(id) { 25 currentDbId = id; 26} 27 28/** 29 * Get the current database ID 30 * @returns {string|null} 31 */ 32export function getDbId() { 33 return currentDbId; 34} 35 36/** 37 * Purge history entries from a previous database instance. 38 * Entries without a db_id field are treated as stale. 39 * @param {string} dbId - Current database ID 40 * @returns {number} Number of entries removed 41 */ 42export function purgeStaleEntries(dbId) { 43 const before = sdkHistory.list().length; 44 const kept = sdkHistory.pruneDbIdMismatch(dbId); 45 return before - kept.length; 46} 47 48/** 49 * Add or update history from a response, after local identifiers are restored. 50 * @param {Object} response - Biometry API response 51 * @param {Object} [options] - Caller-supplied PIN and database context 52 * @param {string} [options.pin] - Retrieved PIN, defaults to the response PIN 53 * @param {string} [options.dbId] - Database ID, defaults to the current database 54 */ 55export function addHistoryEntry(response, { pin = response.biompin?.pin, dbId } = {}) { 56 sdkHistory.add({ 57 pin, 58 patientName: response.data?.patient?.name, 59 patientId: response.data?.patient?.id, 60 expiresAt: response.biompin?.expires_at, 61 dbId: dbId || currentDbId 62 }); 63} 64 65/** 66 * Get all valid (non-expired) history entries 67 * @returns {Array} Array of valid history items 68 */ 69export function getValidHistory() { 70 return sdkHistory.pruneExpired(); 71} 72 73/** 74 * Delete a single history entry by PIN 75 * @param {string} pin - BiomPIN to remove 76 * @returns {number} Remaining entry count 77 */ 78export function deleteHistoryEntry(pin) { 79 const remaining = sdkHistory.clearOne(pin); 80 return remaining.length; 81} 82 83/** 84 * Clear all history 85 */ 86export function clearHistory() { 87 sdkHistory.clearAll(); 88} 89 90/** 91 * Format relative time for expiry display with color coding 92 * @param {string} isoTimestamp - ISO timestamp 93 * @returns {Object} { text: "12h", colorClass: "time-warning" } 94 */ 95function formatRelativeTime(isoTimestamp) { 96 if (!isoTimestamp) { 97 return { text: 'Unknown', colorClass: 'time-unknown' }; 98 } 99 100 const expiry = new Date(isoTimestamp); 101 const now = new Date(); 102 const diffMs = expiry - now; 103 104 if (diffMs < 0) { 105 return { text: 'Expired', colorClass: 'time-expired' }; 106 } 107 108 const diffHours = Math.floor(diffMs / 3600000); 109 110 let colorClass; 111 if (diffHours >= 72) { 112 colorClass = 'time-safe'; // 3+ days: green 113 } else if (diffHours >= 24) { 114 colorClass = 'time-warning'; // 1-3 days: yellow 115 } else { 116 colorClass = 'time-urgent'; // <24 hours: orange 117 } 118 119 let text; 120 if (diffHours >= 24) { 121 const diffDays = Math.floor(diffHours / 24); 122 text = `${diffDays}d`; 123 } else { 124 text = `${diffHours}h`; 125 } 126 127 return { text, colorClass }; 128} 129 130/** 131 * Render history list to DOM 132 * @param {Function} onItemClick - Callback when history item is clicked (receives pin) 133 * @param {string} [filterText] - Optional filter string (matches patient name, patient id, or pin) 134 */ 135export function renderHistory(onItemClick, filterText = '') { 136 const container = document.getElementById('historyList'); 137 if (!container) return; 138 139 const allHistory = getValidHistory(); 140 141 // Apply filter 142 const history = filterText 143 ? allHistory.filter(item => { 144 const q = filterText.toLowerCase(); 145 return (item.patient_name || '').toLowerCase().includes(q) || 146 (item.patient_id || '').toLowerCase().includes(q) || 147 (item.pin || '').toLowerCase().includes(q); 148 }) 149 : allHistory; 150 151 const clearBtn = document.getElementById('clearHistoryBtn'); 152 const searchBar = document.getElementById('historySearchBar'); 153 const hasAny = allHistory.length > 0; 154 155 // Show/hide search bar based on whether history has any entries 156 if (searchBar) searchBar.style.display = hasAny ? '' : 'none'; 157 158 // Empty state 159 if (history.length === 0) { 160 container.innerHTML = ` 161 <div class="history-empty"> 162 <i class="fas fa-clock-rotate-left"></i> 163 <p class="mb-0">
163${hasAny ? 'No matching entries' : 'No BiomPIN history'}</p> 164 </div> 165 `; 166 if (clearBtn) clearBtn.style.display = hasAny ? 'block' : 'none'; 167 return; 168 } 169 170 // Show clear button when there is history 171 if (clearBtn) clearBtn.style.display = 'block'; 172 173 // Determine how many items to show 174 const shouldShowLoadMore = !isHistoryExpanded && history.length > INITIAL_VISIBLE_COUNT; 175 const itemsToShow = shouldShowLoadMore ? history.slice(0, INITIAL_VISIBLE_COUNT) : history; 176 177 // Build history items HTML 178 let html = ''; 179 itemsToShow.forEach(item => { 180 const timeInfo = formatRelativeTime(item.expires_at); 181 182 // Build patient info line - only show available fields 183 const patientParts = []; 184 if (item.patient_name) patientParts.push(item.patient_name); 185 if (item.patient_id) patientParts.push(item.patient_id); 186 const patientInfo = patientParts.length > 0 ? patientParts.join(' ⢠') : ''; 187 188 html += ` 189 <div class="history-item" data-pin="${item.pin}"> 190 <button class="history-item-delete" data-pin="${item.pin}" title="Delete entry" aria-label="Delete ${item.pin}"> 191 <i class="fas fa-trash-can"></i> 192 </button> 193 <div class="history-item-pin">${item.pin}</div> 194 <div class="history-item-info"> 195 <div class="history-item-patient-info"> 196 ${patientInfo} 197 </div> 198 <div class="history-item-time ${timeInfo.colorClass}"> 199 <i class="fas fa-clock"></i> 200 <span>${timeInfo.text}</span> 201 </div> 202 </div> 203 </div> 204 `; 205 }); 206 207 // Add "Load more" card if needed 208 if (shouldShowLoadMore) { 209 const remainingCount = history.length - INITIAL_VISIBLE_COUNT; 210 html += ` 211 <div class="history-load-more" id="historyLoadMore"> 212 <i class="fas fa-chevron-down me-2"></i> 213 <span>Load more from history (${remainingCount} older)</span> 214 </div> 215 `; 216 } 217 218 container.innerHTML = html; 219 220 // Attach delete button handlers 221 container.querySelectorAll('.history-item-delete').forEach(btn => { 222 btn.addEventListener('click', (event) => { 223 event.stopPropagation(); 224 const pin = btn.getAttribute('data-pin'); 225 if (pin) { 226 deleteHistoryEntry(pin); 227 renderHistory(onItemClick, filterText); 228 } 229 }); 230 }); 231 232 // Attach click handlers to history items 233 container.querySelectorAll('.history-item').forEach(item => { 234 item.addEventListener('click', () => { 235 const pin = item.getAttribute('data-pin'); 236 if (pin && onItemClick) { 237 onItemClick(pin); 238 } 239 }); 240 }); 241 242 // Attach click handler to "Load more" card
243 const loadMoreCard = document.getElementById('historyLoadMore'); 244 if (loadMoreCard) { 245 loadMoreCard.addEventListener('click', () => { 246 isHistoryExpanded = true; 247 renderHistory(onItemClick, filterText); 248 }); 249 } 250} 251 252/** 253 * Initialize history UI (attach clear button handler) 254 * @param {Function} onHistoryCleared - Callback when history is cleared 255 */ 256export function initHistoryUI(onHistoryCleared) { 257 const clearBtn = document.getElementById('clearHistoryBtn'); 258 if (!clearBtn) return; 259 260 clearBtn.addEventListener('click', () => { 261 clearHistory(); 262 if (onHistoryCleared) { 263 onHistoryCleared(); 264 } 265 }); 266} 267 268export default { 269 addHistoryEntry, 270 getValidHistory, 271 clearHistory, 272 deleteHistoryEntry, 273 renderHistory, 274 initHistoryUI, 275 setDbId, 276 getDbId, 277 purgeStaleEntries 278};
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.