1/* global fetch, AbortController, setTimeout, clearTimeout, URL */ 2/** 3 * Regulatory update ticker for the tools hub. 4 * 5 * Reads the same cached, credential-free feed the /mcp ledger uses 6 * (https://mcp.bga.ae/public/latest) and shows the newest official UAE 7 * publications the collector has stored. The section starts hidden and is 8 * revealed only once real records render: a catalogue page must never show an 9 * empty or broken "live" block if the feed is unreachable. 10 */ 11(function () { 12 'use strict'; 13 14 var root = document.querySelector('[data-updates-feed]'); 15 if (!root) return; 16 17 var list = root.querySelector('[data-updates-list]'); 18 var stamp = root.querySelector('[data-updates-stamp]'); 19 if (!list) return; 20 21 var language = (document.documentElement.lang || 'en').split('-')[0] === 'ru' ? 'ru' : 'en'; 22 var locale = language === 'ru' ? 'ru-RU' : 'en-GB'; 23 var words = language === 'ru' 24 ? { published: 'опÑбликовано', observed: 'обнаÑÑжено', checked: 'ÐоÑледнÑÑ ÑвеÑка иÑÑоÑников', source: 'ÐÑиÑиалÑнÑй иÑÑоÑник' } 25 : { published: 'published', observed: 'first seen', checked: 'Sources last checked', source: 'Official source' }; 26 27 // Ask for more than we show: secondary discovery records are dropped below, 28 // and the catalogue should still fill its five rows. 29 var FEED = 'https://mcp.bga.ae/public/latest?limit=20'; 30 var SHOWN = 5; 31 var TIMEOUT_MS = 6000; 32 33 function element(tag, className, text) { 34 var node = document.createElement(tag); 35 if (className) node.className = className; 36 if (text != null) node.textContent = text; 37 return node; 38 } 39 40 function safeUrl(value) { 41 try { 42 var url = new URL(value); 43 return url.protocol === 'https:' ? url.href : null; 44 } catch { 45 return null; 46 } 47 } 48 49 function formatDate(value) { 50 if (!value) return null; 51 var date = new Date(value.length === 10 ? value + 'T00:00:00Z' : value); 52 if (Number.isNaN(date.getTime())) return null; 53 return new Intl.DateTimeFormat(locale, { 54 day: 'numeric', month: 'short', year: 'numeric', timeZone: 'Asia/Dubai', 55 }).format(date); 56 } 57 58 function request(url) { 59 var controller = new AbortController(); 60 var timer = setTimeout(function () { controller.abort(); }, TIMEOUT_MS); 61 return fetch(url, { 62 headers: { Accept: 'application/json' }, 63 credentials: 'omit', 64 signal: controller.signal, 65 }).finally(function () { clearTimeout(timer); }); 66 } 67 68 function renderRow(update) { 69 var row = element('li', 'regfeed-item'); 70 71 var published = formatDate(update.published_at); 72 var dateValue = published || formatDate(update.observed_at); 73 if (dateValue) { 74 var meta = element('div', 'regfeed-meta'); 75 var time = element('time', 'regfeed-date', dateValue); 76 time.dateTime = update.published_at || update.observed_at; 77 meta.append(time, element('span', 'regfeed-kind', published ? words.published : words.observed)); 78 row.append(meta); 79 } 80 81 var url = safeUrl(update.canonical_url || update.url); 82 var title = update.title || words.source; 83 var heading; 84 if (url) { 85 heading = element('a', 'regfeed-title', title); 86 heading.href = url; 87 heading.target = '_blank'; 88 heading.rel = 'noopener noreferrer'; 89 } else { 90 heading = element('span', 'regfeed-title', title); 91 } 92 heading.setAttribute('dir', 'auto'); 93 row.append(heading); 94 95 var source = element('span', 'regfeed-source', update.source_name || update.source || words.source); 96 source.setAttribute('dir', 'auto'); 97 row.append(source); 98 return row; 99 } 100 101 request(FEED) 102 .then(function (response) { 103 if (!response.ok) throw new Error('feed ' + response.status); 104 return response.json(); 105 }) 106 .then(function (snapshot) { 107 var all = snapshot && Array.isArray(snapshot.updates) ? snapshot.updates : []; 108 // Only confirmed primary-source records belong on a public catalogue page: 109 // `candidate` rows come from secondary discovery and are not yet verified 110 // against the issuing authority's own listing. 111 var updates = all.filter(function (update) { return update.status !== 'candidate'; }); 112 if (updates.length === 0) throw new Error('no confirmed records'); 113 list.replaceChildren.apply(list, updates.slice(0, SHOWN).map(renderRow)); 114 var checkedAt = snapshot.last_successful_crawl && snapshot.last_successful_crawl.finished_at; 115 if (stamp && checkedAt) { 116 var checked = formatDate(checkedAt); 117 if (checked) stamp.textContent = words.checked + ': ' + checked; 118 } 119 root.hidden = false;
120 }) 121 .catch(function () { 122 // Stay hidden: an unreachable feed must not leave a dead "live" block. 123 root.hidden = true; 124 }); 125})();
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.