status.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. {% extends "base.html" %}
  2. {% set active_page = "assets" %}
  3. {% block title %} {{asset.name}} - Status {% endblock %}
  4. {% block divs %}
  5. {% block breadcrumbs %} {{ super() }} {% endblock %}
  6. <div class="container-fluid">
  7. <div class="row">
  8. <div class="alert alert-info" id="tzwarn" style="display:none;"></div>
  9. <div class="alert alert-info" id="dstwarn" style="display:none;"></div>
  10. </div>
  11. <div class="row">
  12. <div class="col-sm-2"></div>
  13. <div class="col-sm-8">
  14. <div class="card">
  15. <h4>Data connectivity for sensors of {{ asset.name }}
  16. <span class="fa fa-info-circle" title="This table shows the connectivity status for all sensors under or relevant to this asset. Click the table to see the actual data. Per sensor, we report the different data source types. We only explicitly support source types 'demo script', 'user', 'forecaster', 'scheduler' and 'reporter'.
  17. A red traffic light indicates that the last time a record was made was too long ago (it is 'stale'). Hover the light to learn more about why it is displayed red. It could be a source for errors down the line if that sensor data is necessary for forecasts and schedules to be made.
  18. How long ago is considered too long (stale) depends on the sensor, usually we use the sensor's resolution times two. If the source type means we expect future data ('forecaster', 'scheduler'), data should extend 12 hours into the future.
  19. Sensors shown on this page are the ones relevant for correct functioning of the asset. We list the ones linked in the flex-context and the graphs page." style="margin-left: 10px;"></span>
  20. </h4>
  21. <table id="sensor_statuses" class="table table-striped paginate">
  22. <thead>
  23. <tr>
  24. <th>Sensor</th>
  25. <th>Data source type</th>
  26. <th class="no-sort" title="This is the knowledge time of the most recent event recorded (potentially in the future, for forecasts and schedules)">Time of latest record</th>
  27. <th class="text-right no-sort">Status</th>
  28. <th class="d-none">URL</th>
  29. </tr>
  30. </thead>
  31. <tbody>
  32. <!-- This will be populated dynamically with JavaScript -->
  33. </tbody>
  34. </table>
  35. <h4>Latest jobs of {{ asset.name }}
  36. <span class="fa fa-info-circle" title="This table shows forecasting or scheduling jobs for this asset.
  37. A red traffic light indicates something went wrong and should be reported to admins.
  38. Note that jobs do not live forever, so only rather recent jobs (usually younger than a day) are shown at all.
  39. " style="margin-left: 10px;"></span>
  40. <br>
  41. <span class="jobs-time-ago" data-timestamp="Loading">
  42. <small><i class="fa fa-refresh" id="refresh_jobs" title="Refresh status"></i><span id="jobs_time_ago"></span></small>
  43. </span>
  44. </h4>
  45. <table id="scheduling_forecasting_jobs" class="table table-striped paginate nav-on-click">
  46. <thead>
  47. <tr>
  48. <th style="display:none;">Created at Timestamp</th> <!-- Hidden UTC Timestamp column for sorting, Keep at position 0 -->
  49. <th>Created at</th>
  50. <th>Queue</th>
  51. <th>Entity</th>
  52. <th class="text-right no-sort">Status</th>
  53. <th class="text-right">Info</th>
  54. <th class="d-none">URL</th>
  55. </tr>
  56. </thead>
  57. <tbody>
  58. <!-- This will be populated dynamically with JavaScript -->
  59. </tbody>
  60. </table>
  61. <div class="alert alert-warning d-none" id="redis_connection_err"></div>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. <!-- hide control elements -->
  67. <script>
  68. $(document).ready(function() {
  69. $('#sensor_statuses').DataTable({
  70. "searching": false,
  71. "paging": false,
  72. "info": false,
  73. "ordering": false
  74. });
  75. if (sensors.length != 0) {
  76. // Append a loading row to the sensor status table
  77. $('#sensor_statuses tbody').append(getLoadingRow());
  78. }
  79. sensors.forEach(element => {
  80. // Fetch and populate sensor data
  81. getSensorData(element.id);
  82. });
  83. $(document).on('click', '.sensor_refresh', function() {
  84. console.log('Refreshing sensor data');
  85. const sensorId = $(this).attr('id').split('_')[2];
  86. const rowId = `row_${sensorId}`;
  87. // Check if the sensor info text is empty
  88. // This is to determine if we need to show the sensor info or not in case of refresh
  89. const sensorInfoText = $(`#sensor_info_${sensorId}`);
  90. const show_info = (sensorInfoText.html().trim() === "") ? false : true;
  91. // Add Loading Row
  92. $(`#${rowId}`).replaceWith(getLoadingRow(rowId));
  93. // Fetch and update sensor data
  94. getSensorData(sensorId, rowId, show_info);
  95. });
  96. });
  97. var sensors = {{ sensors | tojson }};
  98. var isFirstSensor = true;
  99. var lastSensorName = '';
  100. var assetId = "{{ asset.id }}";
  101. function getSensorData(sensor_id, row_id = null, show_info = false) {
  102. /**
  103. * Fetches sensor data and updates the sensor status table dynamically.
  104. *
  105. * This function sends an AJAX request to fetch the current status of the sensor
  106. * with the given `sensor_id`. Upon receiving the response, it processes the data
  107. * to update the sensor status table. If the `row_id` is provided, it will replace
  108. * the existing row with the updated sensor data; otherwise, it will append a new row
  109. * to the table. The table includes sensor information, source type, staleness time,
  110. * and status, as well as a dynamic "time ago" display indicating when the data was last fetched.
  111. *
  112. * @param {number} sensor_id - The ID of the sensor whose status is being fetched.
  113. * @param {string|null} [row_id=null] - The ID of the table row to update (optional).
  114. * The row_id is basically used to make a refresh call on a particular sensor and then update only
  115. * that row in the table. If not provided, a new row will be appended to the table.
  116. * @param {boolean} [show_info=false] - Whether to show sensor info or not incase of refresh.
  117. *
  118. * @returns {void}
  119. *
  120. * @example
  121. * // To get data for sensor 123 and update the table row with ID 'row_123'
  122. * getSensorData(123, 'row_123');
  123. *
  124. * @example
  125. * // To append a new row for a new sensor with ID 456
  126. * getSensorData(456);
  127. */
  128. let lastCallTime = Date.now();
  129. $.ajax({
  130. url: `/api/v3_0/sensors/${sensor_id}/status`,
  131. method: 'GET',
  132. success: function(response) {
  133. if (isFirstSensor) {
  134. // Clear the table body before appending the first row
  135. $('#sensor_statuses tbody').empty();
  136. isFirstSensor = false;
  137. }
  138. const tbody = $('#sensor_statuses tbody');
  139. response.sensors_data.forEach(function(sensor) {
  140. const isNewSensorName = lastSensorName !== sensor.name;
  141. lastSensorName = sensor.name;
  142. const sensorInfo = (isNewSensorName || show_info)
  143. ? `${sensor.name} (<a href="/sensors/${sensor.id}">${sensor.id}</a>)
  144. <span class="fa fa-info" title="Resolution: ${sensor.resolution}, Asset: '${sensor.asset_name}', reason for listing here: ${sensor.relation}" style="margin-left: 10px;"></span>`
  145. : '';
  146. const source_type = sensor.source_type
  147. ? `<span title="${sensor.source_type}">${sensor.source_type}</span>`
  148. : '<span title="None">None</span>';
  149. const staleness_since = sensor.staleness_since
  150. ? `<span title="${sensor.staleness_since}">${sensor.staleness_since}</span>`
  151. : '<span title="Never">Never</span>';
  152. const refresh_icon = `
  153. <i class="fa fa-refresh sensor_refresh" id="sensor_refresh_${sensor.id}"></i>
  154. `;
  155. const status = sensor.stale
  156. ? `<span title="${sensor.reason}">🔴</span>`
  157. : `<span title="${sensor.reason}">🟢</span>`;
  158. // Add time ago to the fourth column (status column)
  159. const timeAgo = getTimeAgo(lastCallTime);
  160. const row = `
  161. <tr title="View data" id="row_${sensor.id}">
  162. <td id="sensor_info_${sensor.id}">${sensorInfo}</td>
  163. <td>${source_type}</td>
  164. <td>${staleness_since}</td>
  165. <td class="text-right">${status}<br><span class="time-ago" title="Refresh Sensor Status" data-timestamp="${lastCallTime}"><small>${refresh_icon}(${timeAgo})</small></span></td>
  166. <td class="hidden d-none invisible" style="display:none;">/sensors/${sensor.id}</td>
  167. </tr>
  168. `;
  169. // Update the table row if row_id is passed
  170. if (row_id) {
  171. const old_row = $(`#${row_id}`);
  172. old_row.replaceWith(row);
  173. }
  174. else {
  175. tbody.append(row);
  176. }
  177. lastSensorName = sensor.name;
  178. });
  179. },
  180. error: function(xhr) {
  181. console.error('Error fetching sensors:', xhr);
  182. }
  183. });
  184. }
  185. function getAssetJobs(assetId) {
  186. let lastCallTime = Date.now();
  187. console.log('Fetching jobs for asset ID:', assetId);
  188. $.ajax({
  189. url: `/api/v3_0/assets/${assetId}/jobs`,
  190. method: 'GET',
  191. success: function(response) {
  192. // Clear the loading row
  193. $('#scheduling_forecasting_jobs tbody').empty();
  194. // Check if there is redis_connection_err
  195. if (response.redis_connection_err) {
  196. $('#redis_connection_err').removeClass('d-none');
  197. $('#redis_connection_err').text(response.redis_connection_err);
  198. }
  199. else {
  200. $('#redis_connection_err').addClass('d-none');
  201. $('#redis_connection_err').text('');
  202. }
  203. if (response.jobs.length > 0) {
  204. // Clear the table body before appending new data
  205. $('#scheduling_forecasting_jobs tbody').empty();
  206. const tbody = $('#scheduling_forecasting_jobs tbody');
  207. // Loop through the job data and create table rows
  208. response.jobs.forEach(function(job_data) {
  209. const jobStatusMessage = job_data.err === null || job_data.err === undefined
  210. ? job_data.status
  211. : job_data.status + ' : ' + job_data.err;
  212. const jobStatusIcon = job_data.status === 'finished'
  213. ? '🟢'
  214. : job_data.status === 'failed'
  215. ? '🔴'
  216. : '🟡';
  217. const row = `
  218. <tr title="View data">
  219. <td style="display:none;">
  220. ${job_data.enqueued_at}
  221. </td>
  222. <td title="Enqueued at: ${job_data.enqueued_at}">
  223. ${job_data.enqueued_at}
  224. </td>
  225. <td>
  226. ${job_data.queue}
  227. </td>
  228. <td>
  229. ${job_data.entity}
  230. </td>
  231. <td class="text-right"
  232. title="${jobStatusMessage}">
  233. <span>
  234. ${jobStatusIcon}
  235. </span>
  236. </td>
  237. <td class="text-right">
  238. <a href="#" class="btn btn-default btn-success" role="button" id="job-metadata-info-button" data-bs-target="#JobMetadataModal-${job_data.metadata_hash}" data-bs-toggle="modal">
  239. Info
  240. </a>
  241. <div class="modal fade" id="JobMetadataModal-${job_data.metadata_hash}" tabindex="-1" role="dialog" aria-labelledby="JobMetadataInfo" aria-hidden="true">
  242. <div class="modal-dialog" role="document">
  243. <div class="modal-content">
  244. <div class="modal-header">
  245. <h5 class="modal-title" id="JobMetadataInfo">Info</h5>
  246. <button id="modalCloseButton" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onmousedown="event.stopPropagation(); event.preventDefault();"></button>
  247. </div>
  248. <div class="modal-body">
  249. <pre>${job_data.metadata}</pre>
  250. </div>
  251. </div>
  252. </div>
  253. </div>
  254. </td>
  255. <td class="hidden d-none invisible" style="display:none;">
  256. /tasks/0/view/job/${job_data.job_id}
  257. </td>
  258. </tr>
  259. `;
  260. tbody.append(row);
  261. // Update the last call time
  262. const timeAgo = getTimeAgo(lastCallTime);
  263. $('#jobs_time_ago').html(`(${timeAgo})`);
  264. $('.jobs-time-ago').data('timestamp', lastCallTime);
  265. });
  266. }
  267. else {
  268. // Add No Jobs Found message
  269. $('#scheduling_forecasting_jobs tbody').append(`
  270. <tr>
  271. <td colspan="6" class="text-center">
  272. No data available in table.
  273. </td>
  274. </tr>
  275. `);
  276. // Update the last call time
  277. const timeAgo = getTimeAgo(lastCallTime);
  278. $('#jobs_time_ago').html(`(${timeAgo})`);
  279. $('.jobs-time-ago').data('timestamp', lastCallTime);
  280. }
  281. },
  282. error: function(xhr) {
  283. console.error('Error fetching jobs:', xhr);
  284. }
  285. })
  286. }
  287. $(document).ready(function() {
  288. $('#scheduling_forecasting_jobs').DataTable({
  289. "order": [[ 0, "desc" ]], // Default sort by the hidden UTC Timestamp column
  290. "columnDefs": [
  291. {
  292. "targets": 1, // Target the visible "Created at" column
  293. "orderData": 0 // Use data from the first column (UTC Timestamp) for sorting
  294. }
  295. ],
  296. "searching": false,
  297. "paging": false,
  298. "info": false
  299. });
  300. // Add a loading row to the jobs table
  301. $('#scheduling_forecasting_jobs tbody').append(getLoadingRow());
  302. // Initial fetch of jobs
  303. getAssetJobs(assetId);
  304. // Set up an interval to fetch jobs every 60 seconds
  305. // This is to ensure that the table is updated with the latest job data
  306. setInterval(function() {
  307. getAssetJobs(assetId);
  308. }, 60000); // Fetch jobs every 60 seconds
  309. $("#refresh_jobs").click(function() {
  310. getAssetJobs(assetId);
  311. });
  312. // Update time ago dynamically every 10 seconds
  313. setInterval(function() {
  314. $('.time-ago').each(function() {
  315. const timestamp = $(this).data('timestamp');
  316. $(this).html(`<small>${$(this).find('.fa-refresh').prop('outerHTML')} (${getTimeAgo(timestamp)})</small>`);
  317. });
  318. }, 10000);
  319. setInterval(function() {
  320. const timestamp = $('.jobs-time-ago').data('timestamp');
  321. $("#jobs_time_ago").html(`(${getTimeAgo(timestamp)})`);
  322. }, 10000);
  323. });
  324. </script>
  325. {% endblock %}