@@ -185,10 +185,14 @@ function drillDown(level, index, i) {
185185 else selectAndFetch ( "entries" , index ) ;
186186}
187187
188- // Open one entry (global index) in the entry viewer below.
188+ // Open one entry (global index) in the entry viewer below, reusing the bytes
189+ // already fetched as part of its data tile. Falls back to fetching the data
190+ // tile (a standard request) if this entry hasn't been loaded yet.
189191function openEntry ( globalIndex ) {
190192 $ ( "eindex" ) . value = globalIndex ;
191- fetchEntry ( ) ;
193+ const e = entryCache [ globalIndex ] ;
194+ if ( ! e ) { fetchEntry ( ) ; return ; }
195+ renderEntry ( e . bytes , e . label ) ;
192196 $ ( "inspector" ) . scrollIntoView ( { behavior : "smooth" , block : "nearest" } ) ;
193197}
194198
@@ -231,26 +235,42 @@ function renderHashTile(out, url, buf, level, index) {
231235 out . innerHTML = html ;
232236}
233237
234- // Entry (data) tiles are uint16-big-endian length-prefixed entries.
238+ // Split an entry (data) tile into its entries. Data tiles are a sequence of
239+ // uint16-big-endian length-prefixed blobs; subarray() clamps a final
240+ // length that overruns a truncated/partial tile, so this never throws.
241+ function splitDataTile ( buf ) {
242+ const entries = [ ] ;
243+ let pos = 0 ;
244+ while ( pos + 2 <= buf . length ) {
245+ const len = ( buf [ pos ] << 8 ) | buf [ pos + 1 ] ;
246+ pos += 2 ;
247+ entries . push ( buf . subarray ( pos , pos + len ) ) ;
248+ pos += len ;
249+ }
250+ return entries ;
251+ }
252+
253+ // Bytes of entries we've already pulled out of fetched data tiles, keyed by
254+ // global entry index, so clicking an entry link opens it in the viewer with
255+ // no extra fetch. Each value is { bytes, label } (label is the source line
256+ // shown in the inspector header).
257+ const entryCache = { } ;
258+
259+ // Render a fetched entry (data) tile, one clickable row per entry. Clicking a
260+ // row opens that entry in the inspector below straight from the bytes we
261+ // already have — no per-entry request, so this stays standard-API-only.
235262function renderEntriesTile ( out , url , buf , tileIndex ) {
236- let html = '<span class="ok">GET ' + esc ( url ) + "</span>\n" + buf . length + " bytes\n" +
237- '<span class="hint">click an entry to open it in the entry viewer</span>\n\n' ;
238- let pos = 0 , i = 0 ;
239- try {
240- while ( pos + 2 <= buf . length ) {
241- const len = ( buf [ pos ] << 8 ) | buf [ pos + 1 ] ;
242- pos += 2 ;
243- const body = buf . subarray ( pos , pos + len ) ;
244- pos += len ;
245- const preview = hex ( body . subarray ( 0 , 32 ) ) ;
246- const gidx = tileIndex * TILE_W + i ;
247- html += '<a href="#" onclick="openEntry(' + gidx + ');return false">entry ' + i + "</a>" +
248- " (#" + gidx + "): " + len + " bytes " + preview + ( len > 32 ? "…" : "" ) + "\n" ;
249- i ++ ;
250- }
251- html = html . replace ( "\n\n" , "\n" + i + " entr" + ( i === 1 ? "y" : "ies" ) + "\n\n" ) ;
252- } catch ( e ) {
253- html += '<span class="err">decode error: ' + esc ( String ( e . message ) ) + "</span>" ;
263+ const entries = splitDataTile ( buf ) ;
264+ let html = '<span class="ok">GET ' + esc ( url ) + "</span>\n" + buf . length + " bytes · " +
265+ entries . length + " entr" + ( entries . length === 1 ? "y" : "ies" ) + "\n" +
266+ '<span class="hint">click an entry to open it in the entry viewer below</span>\n\n' ;
267+ for ( let i = 0 ; i < entries . length ; i ++ ) {
268+ const body = entries [ i ] ;
269+ const gidx = tileIndex * TILE_W + i ;
270+ entryCache [ gidx ] = { bytes : body , label : url + " · entry " + i + " (#" + gidx + ")" } ;
271+ const preview = hex ( body . subarray ( 0 , 32 ) ) ;
272+ html += '<a href="#" onclick="openEntry(' + gidx + ');return false">entry ' + i + "</a>" +
273+ " (#" + gidx + "): " + body . length + " bytes " + preview + ( body . length > 32 ? "…" : "" ) + "\n" ;
254274 }
255275 out . innerHTML = html ;
256276}
@@ -677,12 +697,14 @@ function renderAnnotations(container, ann) {
677697 container . innerHTML = ann . length ? html : '<div class="hint">(no annotations)</div>' ;
678698}
679699
680- // Render the full three-column inspector for one entry blob.
681- function renderEntry ( buf , url ) {
700+ // Render the full three-column inspector for one entry blob. `label` is the
701+ // source line shown in the header (e.g. the data-tile path the entry came
702+ // from).
703+ function renderEntry ( buf , label ) {
682704 const res = parseEntry ( buf ) ;
683705 const meta = $ ( "entrymeta" ) ;
684706 meta . style . display = "block" ;
685- let m = '<span class="ok">GET ' + esc ( url ) + "</span> · " + buf . length + " bytes (MerkleTreeCertEntry)" ;
707+ let m = '<span class="ok">' + esc ( label ) + "</span> · " + buf . length + " bytes (MerkleTreeCertEntry)" ;
686708 if ( res . typeLabel ) m += " · type " + esc ( res . typeLabel ) ;
687709 if ( res . error ) m += ' · <span class="err">' + esc ( res . error ) + "</span>" ;
688710 meta . innerHTML = m ;
@@ -694,9 +716,15 @@ function renderEntry(buf, url) {
694716 return res ;
695717}
696718
719+ // Look up an entry by global index using only the standard tile API: fetch
720+ // the data tile that contains it (auto-sizing the partial width from the
721+ // checkpoint) and pull out the entry at its position within that tile.
697722async function fetchEntry ( ) {
698723 const idx = parseInt ( $ ( "eindex" ) . value , 10 ) || 0 ;
699- const url = "log/v1/entry/" + idx ;
724+ const tileIndex = Math . floor ( idx / TILE_W ) ;
725+ const posInTile = idx % TILE_W ;
726+ const width = tileWidth ( "entries" , tileIndex ) ;
727+ const url = tileURL ( "entries" , tileIndex , width || null ) ;
700728 const meta = $ ( "entrymeta" ) ;
701729 meta . style . display = "block" ;
702730 meta . textContent = "GET " + url + " …" ;
@@ -705,7 +733,15 @@ async function fetchEntry() {
705733 const r = await fetch ( url , { cache : "no-store" } ) ;
706734 if ( ! r . ok ) { meta . innerHTML = '<span class="err">GET ' + esc ( url ) + " → HTTP " + r . status + "</span>" ; return ; }
707735 const buf = new Uint8Array ( await r . arrayBuffer ( ) ) ;
708- renderEntry ( buf , url ) ;
736+ const entries = splitDataTile ( buf ) ;
737+ if ( posInTile >= entries . length ) {
738+ meta . innerHTML = '<span class="err">entry #' + idx + " not in " + esc ( url ) +
739+ " (tile holds " + entries . length + " entr" + ( entries . length === 1 ? "y" : "ies" ) + ")</span>" ;
740+ return ;
741+ }
742+ const body = entries [ posInTile ] ;
743+ entryCache [ idx ] = { bytes : body , label : url + " · entry " + posInTile + " (#" + idx + ")" } ;
744+ renderEntry ( body , entryCache [ idx ] . label ) ;
709745 } catch ( e ) {
710746 meta . innerHTML = '<span class="err">' + esc ( String ( e . message ) ) + "</span>" ;
711747 }
@@ -789,7 +825,7 @@ if (typeof module !== "undefined" && module.exports) {
789825 module . exports = {
790826 TILE_W , formatTileIndex, tileURL, treeHeight, tileLevels, tileWidth,
791827 loadCheckpoint, renderTileLists, populateLevels, selectAndFetch, drillDown,
792- openEntry, fetchTile, renderHashTile, renderEntriesTile, fetchEntry,
828+ openEntry, fetchTile, renderHashTile, splitDataTile , renderEntriesTile, fetchEntry,
793829 DER , tagName, decodeOID, oidName, decodeDN, decodeSAN, hexPreview,
794830 previewInteger, formatTime, previewPrimitive, derNode, derForest,
795831 parseEntry, renderHexDump, renderStructure, renderAnnotations, renderEntry,
0 commit comments