PageSourceSearch

https://bga.ae/service-assets/updates-feed.c150cbb2.js

js bga.ae collected 2026-10-06 06:59:24 UTC 4,833 bytes, 125 lines download raw bytes

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.