Changeset 3763178
- Timestamp:
- 10/24/2026 11:15:37 AM (69 minutes ago)
- Location:
- qndrs-availability-heartbeat-monitor/trunk
- Files:
-
- 11 edited
-
README.md (modified) (5 diffs)
-
css/uptime-monitor.css (modified) (3 diffs)
-
docs/history-storage.md (modified) (2 diffs)
-
js/uptime-monitor.js (modified) (8 diffs)
-
languages/qndrs-availability-heartbeat-monitor-nl_NL.l10n.php (modified) (11 diffs)
-
languages/qndrs-availability-heartbeat-monitor-nl_NL.mo (modified) ( previous)
-
languages/qndrs-availability-heartbeat-monitor-nl_NL.po (modified) (2 diffs)
-
qndrs-availability-heartbeat-monitor.php (modified) (29 diffs)
-
readme.txt (modified) (3 diffs)
-
uptime-monitor.css (modified) (3 diffs)
-
uptime-monitor.js (modified) (8 diffs)
Legend:
- Unmodified
- Added
- Removed
-
qndrs-availability-heartbeat-monitor/trunk/README.md
r3666118 r3763178 14 14 - Tested up to: WordPress 7.1 15 15 - Requires PHP: 8.0+ 16 - Stable tag: 3. 6.116 - Stable tag: 3.7.1 17 17 - License: GPLv2 or later 18 18 - License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 32 32 - Monitor firewalled machines or internal apps with outbound heartbeat pings. 33 33 - Dutch translation files are included. 34 - Choose a light, dark or system theme for the monitor overview. 35 - Show or hide overview columns, with a compact preset and a reset to the full view. 36 37 ## Overview Display 38 39 Use **Light / Dark / System** and **Columns** above the overview to choose your display. Last check, response time, uptime, notifications and incidents can be switched independently. Monitor type and URL/heartbeat interval can also be hidden. Status, monitor name and management actions remain available. 40 41 The initial view is light with all columns visible. Display preferences are saved in this browser for the current site and WordPress user, and survive automatic refresh. If browser storage is unavailable, the controls still work for the current page. System mode follows the operating system's color preference. The dark theme applies to the monitor overview; the settings page, WordPress menu and dashboard widget keep their existing appearance. 34 42 35 43 ## Installation … … 37 45 1. Upload the plugin folder to `/wp-content/plugins/qndrs-availability-heartbeat-monitor`. 38 46 2. Activate **Qndrs Availability and Heartbeat Monitor** from the WordPress plugins page. 39 3. Open **Qndrs Monitor ** in the WordPress admin menu.47 3. Open **Qndrs Monitor > Overview** in the WordPress admin menu. Use **Qndrs Monitor > Settings** for configuration. 40 48 4. Add one or more URLs and choose the notification channels. 41 49 5. Optional: use a real server cron for more reliable checks: … … 142 150 The checks validate PHP syntax, JavaScript syntax when Node.js is available, version consistency, and release packaging basics. 143 151 152 Run the standalone reliability regression checks with `php scripts/check-reliability.php`. These use WordPress and database doubles to cover migration recovery, aggregate status and concurrent heartbeat writes; they do not replace WordPress/MySQL integration testing. 153 154 Run `php scripts/check-overview.php` to also check the production overview renderer. For browser checks with the actual PHP output, JavaScript, CSS and an AJAX fixture, see `docs/dashboard-design.md`. 155 156 Overall status is `unknown` while any active monitor has no result, or when there are no active monitors. Incidents and degraded results take precedence. Concurrent heartbeat updates merge with the latest stored monitor list; database failures or exhausted retries return HTTP 503 for the sender to retry. 157 144 158 ### Dutch translation maintenance 145 159 … … 153 167 154 168 ## Changelog 169 170 ### 3.7.1 171 172 - Renamed the first submenu to Overview to distinguish it from the Qndrs Monitor parent menu; Settings remains the configuration page. 173 174 ### 3.7.0 175 176 - Added light, dark and system themes with configurable overview columns and a compact view. 177 - Preserved display preferences and expanded history during automatic refresh. 178 - Fixed shadow-history migration recovery, unknown aggregate status and concurrent heartbeat persistence. 179 - Added Dutch translations for the new display controls. 155 180 156 181 ### 3.6.1 -
qndrs-availability-heartbeat-monitor/trunk/css/uptime-monitor.css
r3608284 r3763178 18 18 } 19 19 20 /* Overview preferences are deliberately scoped away from settings and the widget. */ 21 .uptime-monitor-overview { 22 --ump-columns: 90px minmax(190px, 1.4fr) minmax(150px, .85fr) minmax(90px, .5fr) minmax(190px, 1fr) minmax(100px, .6fr) minmax(100px, .6fr) 340px; 23 --ump-list-min-width: 1386px; 24 --ump-soft: #f6f7f7; 25 --ump-link: #2271b1; 26 --ump-shadow: 0 12px 34px rgba(0, 0, 0, .16); 27 } 28 29 .uptime-monitor-overview [hidden] { display: none !important; } 30 .uptime-view-controls { align-items: center; display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end; margin: 12px 0; } 31 .uptime-theme-switch { background: var(--ump-panel); border: 1px solid var(--ump-border); border-radius: 6px; display: flex; padding: 3px; } 32 .uptime-theme-switch button { background: transparent; border: 0; border-radius: 3px; color: var(--ump-muted); cursor: pointer; font: inherit; padding: 6px 12px; } 33 .uptime-theme-switch button[aria-pressed="true"] { background: var(--ump-soft); color: var(--ump-text); box-shadow: 0 1px 3px rgba(0, 0, 0, .15); } 34 .uptime-monitor-overview :is(button, summary, input, a):focus-visible { outline: 2px solid var(--ump-link); outline-offset: 2px; } 35 .uptime-column-settings { position: relative; } 36 .uptime-column-settings > summary { cursor: pointer; list-style: none; } 37 .uptime-column-settings > summary::-webkit-details-marker { display: none; } 38 .uptime-column-settings[open] > summary { border-color: var(--ump-link); } 39 .uptime-column-count { border-left: 1px solid var(--ump-border); color: var(--ump-muted); font-size: 11px; margin-left: 6px; padding-left: 8px; } 40 .uptime-column-panel { background: var(--ump-panel); border: 1px solid var(--ump-border); border-radius: 6px; box-shadow: var(--ump-shadow); box-sizing: border-box; max-height: 65vh; overflow-y: auto; padding: 16px; position: absolute; right: 0; top: calc(100% + 8px); width: 310px; z-index: 20; } 41 .uptime-column-panel p { color: var(--ump-muted); font-size: 12px; margin: 0 0 10px; } 42 .uptime-column-panel fieldset { border: 0; margin: 0; min-width: 0; padding: 0; } 43 .uptime-column-panel label { align-items: center; cursor: pointer; display: flex; gap: 9px; padding: 7px 0; } 44 .uptime-column-panel input[type="checkbox"] { flex: 0 0 auto; margin: 0; } 45 .uptime-column-panel-actions { border-top: 1px solid var(--ump-border); display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; padding-top: 12px; } 46 .uptime-column-panel-actions .button { flex: 1 1 auto; } 47 .uptime-url-list-scroll { overflow-x: auto; padding: 2px 2px 8px; scrollbar-color: var(--ump-border) transparent; } 48 .uptime-url-column-head { align-items: center; box-sizing: border-box; color: var(--ump-muted); display: grid; font-size: 11px; font-weight: 600; gap: 12px 14px; grid-template-columns: var(--ump-columns); min-width: var(--ump-list-min-width); padding: 8px 17px 8px 21px; } 49 .uptime-monitor-overview .uptime-url-list { min-width: var(--ump-list-min-width); } 50 .uptime-url-list-scroll.is-empty .uptime-url-column-head { display: none; } 51 .uptime-monitor-overview .uptime-url-list-scroll.is-empty .uptime-url-list { min-width: 0; } 52 .uptime-monitor-overview .uptime-url-row { gap: 12px 14px; grid-template-columns: var(--ump-columns); } 53 .uptime-monitor-overview .uptime-url-actions { justify-content: flex-end; } 54 .uptime-url-response, .uptime-url-incident { align-items: center; display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; } 55 .uptime-hide-check [data-view-column="check"], 56 .uptime-hide-response [data-view-column="response"], 57 .uptime-hide-uptime [data-view-column="uptime"], 58 .uptime-hide-alerts [data-view-column="alerts"], 59 .uptime-hide-incident [data-view-column="incident"], 60 .uptime-hide-type [data-view-column="type"], 61 .uptime-hide-address [data-view-column="address"] { display: none !important; } 62 63 .uptime-monitor-overview[data-theme="dark"] { 64 --ump-bg: #10161f; 65 --ump-panel: #19222e; 66 --ump-soft: #202c3a; 67 --ump-border: #344152; 68 --ump-text: #e7edf5; 69 --ump-muted: #a9b6c7; 70 --ump-up: #6ad79a; 71 --ump-down: #ff9d9d; 72 --ump-paused: #a9b6c7; 73 --ump-warning: #f0c36d; 74 --ump-info: #9bbcff; 75 --ump-link: #9bbcff; 76 --ump-shadow: 0 12px 34px rgba(0, 0, 0, .4); 77 background: var(--ump-bg); 78 color-scheme: dark; 79 } 80 body.qndrs-ahm-dark-overview #wpcontent, 81 body.qndrs-ahm-dark-overview #wpfooter { background: #10161f; } 82 body.qndrs-ahm-dark-overview #wpfooter { color: #a9b6c7; } 83 body.qndrs-ahm-dark-overview #wpfooter a { color: #9bbcff; } 84 .uptime-monitor-overview[data-theme="dark"] :is(h1, h2, h3, label) { color: var(--ump-text); } 85 .uptime-monitor-overview[data-theme="dark"] a { color: var(--ump-link); } 86 .uptime-monitor-overview[data-theme="dark"] .button { background: var(--ump-panel); border-color: var(--ump-border); color: var(--ump-link); } 87 .uptime-monitor-overview[data-theme="dark"] .button:hover { background: var(--ump-soft); border-color: var(--ump-link); } 88 .uptime-monitor-overview[data-theme="dark"] .button-primary { background: #5269ee; border-color: #5269ee; color: #fff; } 89 .uptime-monitor-overview[data-theme="dark"] .button-primary:hover { background: #4359d1; border-color: #4359d1; } 90 .uptime-monitor-overview[data-theme="dark"] .button:disabled { background: var(--ump-soft) !important; border-color: var(--ump-border) !important; color: var(--ump-muted) !important; } 91 .uptime-monitor-overview[data-theme="dark"] :is(input[type="url"], input[type="text"], input[type="checkbox"]) { background: var(--ump-bg); border-color: var(--ump-muted); color: var(--ump-text); } 92 .uptime-monitor-overview[data-theme="dark"] input::placeholder { color: var(--ump-muted); } 93 .uptime-monitor-overview[data-theme="dark"] .uptime-chip { background: var(--ump-soft); border-color: var(--ump-border); color: var(--ump-text); } 94 .uptime-monitor-overview[data-theme="dark"] .uptime-chip-muted { color: var(--ump-muted); } 95 .uptime-monitor-overview[data-theme="dark"] .uptime-chip.is-warning { background: #382e1d; border-color: #706044; color: var(--ump-warning); } 96 .uptime-monitor-overview[data-theme="dark"] .uptime-chip.is-danger { background: #382329; border-color: #70404b; color: var(--ump-down); } 97 .uptime-monitor-overview[data-theme="dark"] .uptime-percentage { background: var(--ump-soft); border-color: var(--ump-border); } 98 .uptime-monitor-overview[data-theme="dark"] .uptime-percentage-label { background: #2b3747; color: var(--ump-muted); } 99 .uptime-monitor-overview[data-theme="dark"] .uptime-percentage-value { color: var(--ump-up); } 100 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-response-average, .uptime-history-response-time) { background: #20364a; color: #acdcff; } 101 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-history-empty, .uptime-history-time, .uptime-history-message) { color: var(--ump-muted); } 102 .uptime-monitor-overview[data-theme="dark"] .uptime-history-code { color: var(--ump-text); } 103 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-status-up, .uptime-response-trend-faster) { background: #213b30; color: var(--ump-up); } 104 .uptime-monitor-overview[data-theme="dark"] .uptime-status-down { background: #44282f; color: var(--ump-down); } 105 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-status-error, .uptime-status-degraded, .uptime-response-trend-slower) { background: #382e1d; color: var(--ump-warning); } 106 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-status-paused, .uptime-status-unknown, .uptime-response-trend-stable) { background: var(--ump-soft); color: var(--ump-muted); } 107 .uptime-monitor-overview[data-theme="dark"] .uptime-delete-button { border-color: #70404b; color: var(--ump-down); } 108 .uptime-monitor-overview[data-theme="dark"] .uptime-delete-button:hover { background: #44282f; border-color: var(--ump-down); } 109 .uptime-monitor-overview[data-theme="dark"] .uptime-monitor-notice { background: var(--ump-panel); color: var(--ump-text); } 110 .uptime-monitor-overview[data-theme="dark"] .notice-dismiss::before { color: var(--ump-muted); } 111 112 @media (max-width: 782px) { 113 .uptime-monitor-overview .uptime-dashboard-header { align-items: flex-start; flex-direction: column; } 114 .uptime-view-controls { justify-content: space-between; } 115 .uptime-column-settings { margin-left: auto; } 116 .uptime-column-panel { max-width: calc(100vw - 40px); } 117 } 118 20 119 .uptime-widget-refresh-meter { 21 120 background: rgba(34, 113, 177, 0.12); … … 62 161 border-left-color: var(--ump-warning); 63 162 background: rgba(183, 121, 31, 0.07); 163 } 164 165 .uptime-dashboard-widget.is-unknown .uptime-widget-status { 166 border-left-color: var(--ump-paused); 64 167 } 65 168 … … 219 322 .uptime-statusbar.is-degraded { 220 323 border-left-color: var(--ump-warning); 324 } 325 326 .uptime-statusbar.is-unknown { 327 border-left-color: var(--ump-paused); 221 328 } 222 329 -
qndrs-availability-heartbeat-monitor/trunk/docs/history-storage.md
r3651690 r3763178 36 36 ``` 37 37 38 The migration uses idempotent batched inserts and switches to `table_shadow` only when the valid legacy count exactly matches the table count. The legacy option is retained at this stage. 38 Pause scheduled checks and heartbeat senders and let in-flight writes finish before running migration, rollback or finalization. These maintenance commands compare snapshots and must not overlap history writes. 39 40 Migration rebuilds the unfinalized table copy from legacy history using batched inserts and switches to `table_shadow` only when the valid legacy count exactly matches the table count. Rerunning migration repairs extra rows left by earlier retention or interrupted migrations. Failed imports leave legacy reads active. Migration refuses to rebuild finalized table storage. The legacy option is retained at this stage. 39 41 40 42 Compare the dashboard metrics and recent history before finalizing. Roll back while shadow mode is active with: … … 56 58 The dashboard loads only the five newest rows per monitor. Uptime totals and response-time statistics are calculated with indexed aggregate queries rather than by loading all history into PHP memory. 57 59 58 Rows older than 30 days are removed at most once per hour when a new history event is stored.60 In table-only mode, rows older than 30 days are removed at most once per hour when a new history event is stored. During shadow verification, neither copy is pruned, including the legacy per-monitor entry cap, so the two counts remain comparable. Finalize after verification to resume retention cleanup; leaving shadow mode active indefinitely allows history to grow. -
qndrs-availability-heartbeat-monitor/trunk/js/uptime-monitor.js
r3651690 r3763178 6 6 let widgetRefreshTimer = null; 7 7 let widgetRefreshRequest = null; 8 const overview = $('.uptime-monitor-overview'); 9 const viewPreferences = { theme: 'light', hidden: [] }; 10 const viewColumns = { 11 check: { track: 'minmax(150px, .85fr)', width: 164 }, 12 response: { track: 'minmax(90px, .5fr)', width: 104 }, 13 uptime: { track: 'minmax(190px, 1fr)', width: 204 }, 14 alerts: { track: 'minmax(100px, .6fr)', width: 114 }, 15 incident: { track: 'minmax(100px, .6fr)', width: 114 } 16 }; 17 const optionalViewFields = [...Object.keys(viewColumns), 'type', 'address']; 18 const systemTheme = window.matchMedia('(prefers-color-scheme: dark)'); 19 20 function applyViewTheme() { 21 if (!overview.length) { 22 return; 23 } 24 const theme = viewPreferences.theme === 'system' ? (systemTheme.matches ? 'dark' : 'light') : viewPreferences.theme; 25 overview.attr('data-theme', theme); 26 $('body').toggleClass('qndrs-ahm-dark-overview', theme === 'dark'); 27 overview.find('[data-theme-choice]').each(function () { 28 $(this).attr('aria-pressed', String(this.dataset.themeChoice === viewPreferences.theme)); 29 }); 30 } 31 32 function applyViewColumns() { 33 if (!overview.length) { 34 return; 35 } 36 const visible = Object.keys(viewColumns).filter(key => !viewPreferences.hidden.includes(key)); 37 overview[0].style.setProperty('--ump-columns', [ 38 '90px', 'minmax(190px, 1.4fr)', ...visible.map(key => viewColumns[key].track), '340px' 39 ].join(' ')); 40 overview[0].style.setProperty('--ump-list-min-width', (686 + visible.reduce((width, key) => width + viewColumns[key].width, 0)) + 'px'); 41 optionalViewFields.forEach(function (key) { 42 const hidden = viewPreferences.hidden.includes(key); 43 overview.toggleClass('uptime-hide-' + key, hidden); 44 overview.find('[data-view-toggle="' + key + '"]').prop('checked', !hidden); 45 }); 46 overview.find('.uptime-column-count').text((optionalViewFields.length - viewPreferences.hidden.length) + '/' + optionalViewFields.length); 47 } 48 49 function saveViewPreferences() { 50 try { 51 window.localStorage.setItem(qndrsAhmAjax.view_storage_key, JSON.stringify(viewPreferences)); 52 } catch (error) { 53 // Preferences still work for this page when browser storage is unavailable. 54 } 55 } 56 57 function initializeViewPreferences() { 58 if (!overview.length) { 59 return; 60 } 61 try { 62 const saved = JSON.parse(window.localStorage.getItem(qndrsAhmAjax.view_storage_key)); 63 if (saved && ['light', 'dark', 'system'].includes(saved.theme)) { 64 viewPreferences.theme = saved.theme; 65 } 66 if (saved && Array.isArray(saved.hidden)) { 67 viewPreferences.hidden = optionalViewFields.filter(key => saved.hidden.includes(key)); 68 } 69 } catch (error) { 70 // Corrupt or blocked storage must not prevent monitoring controls from working. 71 } 72 applyViewTheme(); 73 applyViewColumns(); 74 overview.find('.uptime-view-controls').prop('hidden', false); 75 overview.on('click', '[data-theme-choice]', function () { 76 viewPreferences.theme = this.dataset.themeChoice; 77 applyViewTheme(); 78 saveViewPreferences(); 79 }); 80 overview.on('change', '[data-view-toggle]', function () { 81 viewPreferences.hidden = optionalViewFields.filter(key => !overview.find('[data-view-toggle="' + key + '"]').prop('checked')); 82 applyViewColumns(); 83 saveViewPreferences(); 84 }); 85 overview.on('click', '.uptime-view-compact, .uptime-view-reset', function () { 86 viewPreferences.hidden = $(this).hasClass('uptime-view-compact') ? ['check', 'alerts', 'incident', 'type', 'address'] : []; 87 applyViewColumns(); 88 saveViewPreferences(); 89 }); 90 $(document).on('click.qndrsAhmView', function (event) { 91 const settings = overview.find('.uptime-column-settings'); 92 if (!settings[0].contains(event.target)) { 93 settings.prop('open', false); 94 } 95 }).on('keydown.qndrsAhmView', function (event) { 96 const settings = overview.find('.uptime-column-settings'); 97 if (event.key === 'Escape' && settings.prop('open')) { 98 settings.prop('open', false).find('summary').trigger('focus'); 99 } 100 }); 101 systemTheme.addEventListener('change', applyViewTheme); 102 } 103 104 initializeViewPreferences(); 8 105 9 106 function escapeHtml(value) { … … 205 302 206 303 const statusCode = parseInt(latest.status_code, 10); 207 const responseTime = parseInt(latest.response_time_ms, 10);208 304 const timestamp = latest.timestamp || ''; 209 305 const displayTimestamp = latest.display_timestamp || timestamp; … … 211 307 return ` 212 308 ${statusCode ? `<span class="uptime-chip">${escapeHtml(qndrsAhmL10n.http_status.replace('%d', statusCode))}</span>` : ''} 213 ${!isNaN(responseTime) ? `<span class="uptime-chip">${escapeHtml(qndrsAhmL10n.response_time_ms.replace('%d', responseTime))}</span>` : ''}214 309 ${timestamp ? `<time class="uptime-chip uptime-chip-muted" datetime="${escapeHtml(timestamp)}">${escapeHtml(displayTimestamp)}</time>` : ''} 215 310 `; … … 229 324 const title = type === 'heartbeat' ? (urlData.name || qndrsAhmL10n.heartbeat) : getHost(url); 230 325 const identityMeta = type === 'heartbeat' 231 ? `<span >${escapeHtml((qndrsAhmL10n.expected_every_seconds || 'Expected every %d seconds').replace('%d', parseInt(urlData.heartbeat && urlData.heartbeat.expected_interval ? urlData.heartbeat.expected_interval : urlData.expected_interval, 10) || 300))}</span>`232 : `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`;326 ? `<span data-view-column="address">${escapeHtml((qndrsAhmL10n.expected_every_seconds || 'Expected every %d seconds').replace('%d', parseInt(urlData.heartbeat && urlData.heartbeat.expected_interval ? urlData.heartbeat.expected_interval : urlData.expected_interval, 10) || 300))}</span>` 327 : `<a data-view-column="address" href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`; 233 328 const averageResponse = dashboard.average_response_time_display 234 329 ? `<span class="uptime-response-average">${escapeHtml(dashboard.average_response_time_display)}</span>` 235 330 : ''; 331 const latest = dashboard.latest || {}; 332 const responseTime = parseInt(latest.response_time_ms, 10); 333 const currentResponse = !isNaN(responseTime) 334 ? `<span class="uptime-chip">${escapeHtml(qndrsAhmL10n.response_time_ms.replace('%d', responseTime))}</span>` 335 : (averageResponse ? '' : `<span class="uptime-chip uptime-chip-muted">${escapeHtml(qndrsAhmL10n.no_data)}</span>`); 236 336 237 337 return ` … … 243 343 <div class="uptime-url-identity"> 244 344 <strong>${escapeHtml(title)}</strong> 245 <span class="uptime-chip uptime-chip-muted" >${escapeHtml(type === 'heartbeat' ? qndrsAhmL10n.heartbeat : qndrsAhmL10n.http_check)}</span>345 <span class="uptime-chip uptime-chip-muted" data-view-column="type">${escapeHtml(type === 'heartbeat' ? qndrsAhmL10n.heartbeat : qndrsAhmL10n.http_check)}</span> 246 346 ${identityMeta} 247 347 </div> 248 <div class="uptime-url-current" >348 <div class="uptime-url-current" data-view-column="check"> 249 349 ${renderCurrentCheck(dashboard)} 250 350 </div> 251 <div class="uptime-url-metrics"> 351 <div class="uptime-url-response" data-view-column="response"> 352 ${currentResponse} 353 ${averageResponse} 354 </div> 355 <div class="uptime-url-metrics" data-view-column="uptime"> 252 356 ${renderUptimeSummary(urlData.uptime)} 253 ${averageResponse} 254 </div> 255 <div class="uptime-url-alerts"> 357 </div> 358 <div class="uptime-url-alerts" data-view-column="alerts"> 256 359 <span class="uptime-chip${notificationsLabel === qndrsAhmL10n.no_alerts ? ' is-warning' : ''}">${escapeHtml(notificationsLabel)}</span> 360 </div> 361 <div class="uptime-url-incident" data-view-column="incident"> 257 362 <span class="uptime-chip${incidentOpen ? ' is-danger' : ' uptime-chip-muted'}">${escapeHtml(incidentLabel)}</span> 258 363 ${incidentDuration ? `<span class="uptime-chip is-danger">${escapeHtml(incidentDuration)}</span>` : ''} … … 272 377 function updateUrlList(urls) { 273 378 const list = $('.uptime-url-list'); 379 list.closest('.uptime-url-list-scroll').toggleClass('is-empty', !Array.isArray(urls) || urls.length === 0); 274 380 const expandedIds = []; 275 381 … … 466 572 467 573 $('.uptime-auto-refresh-input').prop('checked', isEnabled); 468 window.localStorage.setItem(autoRefreshStorageKey, isEnabled ? '1' : '0'); 574 try { 575 window.localStorage.setItem(autoRefreshStorageKey, isEnabled ? '1' : '0'); 576 } catch (error) { 577 // Auto-refresh remains usable without persistent browser storage. 578 } 469 579 470 580 if (isEnabled) { … … 511 621 }); 512 622 513 if ($('.uptime-auto-refresh-input').length && window.localStorage.getItem(autoRefreshStorageKey) === '1') { 514 setAutoRefresh(true); 623 try { 624 if ($('.uptime-auto-refresh-input').length && window.localStorage.getItem(autoRefreshStorageKey) === '1') { 625 setAutoRefresh(true); 626 } 627 } catch (error) { 628 // Keep manual refresh and all other controls available. 515 629 } 516 630 -
qndrs-availability-heartbeat-monitor/trunk/languages/qndrs-availability-heartbeat-monitor-nl_NL.l10n.php
r3666118 r3763178 1 1 <?php 2 2 return array ( 3 'project-id-version' => 'Qndrs Availability and Heartbeat Monitor 3. 6.1',3 'project-id-version' => 'Qndrs Availability and Heartbeat Monitor 3.7.1', 4 4 'report-msgid-bugs-to' => '', 5 5 'po-revision-date' => '2026-08-26 12:00+0200', … … 23 23 'A general error occurred. Please try again.' => 'Er is een algemene fout opgetreden. Probeer het opnieuw.', 24 24 'Across URLs with history' => 'Over URL\'s met geschiedenis', 25 'Actions' => 'Acties', 25 26 'Active incidents: %1$d; active URLs: %2$d' => 'Actieve incidenten: %1$d; actieve URLs: %2$d', 26 27 'Add URL' => 'URL toevoegen', … … 39 40 'Clear stored user key' => 'Opgeslagen gebruikerssleutel wissen', 40 41 'Clients send a POST request to this endpoint with their own heartbeat token.' => 'Clients sturen een POST naar dit endpoint met hun eigen heartbeat token.', 42 'Color theme' => 'Kleurthema', 43 'Columns' => 'Kolommen', 44 'Compact overview' => 'Compact overzicht', 41 45 'Complete monitor status response.' => 'Reactie met de volledige monitorstatus.', 42 46 'Configuration Import / Export' => 'Configuratie importeren / exporteren', … … 59 63 'Current settings' => 'Huidige instellingen', 60 64 'Current token ends with %s.' => 'Huidig token eindigt op %s.', 65 'Dark' => 'Donker', 61 66 'Degraded' => 'Verminderd', 62 67 'Delete' => 'Verwijderen', … … 107 112 'Incident active' => 'Incident actief', 108 113 'Incident open' => 'Incident open', 114 'Incidents' => 'Incidenten', 109 115 'Interval:' => 'Interval:', 110 116 'Invalid JSON format.' => 'Ongeldige JSON format.', … … 114 120 'Invalid form request.' => 'Ongeldige formulieraanvraag.', 115 121 'Invalid log date parameter: %s.' => 'Ongeldige datum parameter %s in log.', 122 'Last check' => 'Laatste check', 123 'Last check & HTTP status' => 'Laatste check & HTTP-status', 116 124 'Last check:' => 'Laatste check:', 117 125 'Last ping:' => 'Laatste ping:', 126 'Light' => 'Licht', 118 127 'Logs' => 'Logs', 119 128 'Maximum wait time per HTTP request attempt.' => 'Maximale wachttijd per poging tot een HTTP-aanvraag.', 120 129 'Minimum 60 seconds' => 'Minimaal 60 seconden', 121 130 'Missing for %s' => 'Ontbrekend sinds %s', 131 'Monitor' => 'Monitor', 122 132 'Monitor Interval (seconds)' => 'Monitorinterval (seconden)', 123 133 'Monitor interval' => 'Monitorinterval', 124 134 'Monitor status by ID.' => 'Status van een monitor op basis van ID.', 135 'Monitor type' => 'Type monitor', 125 136 'Monitor website availability and receive alerts by email or Pushover. Manage multiple URLs from the WordPress admin with logging, JSON import/export, REST support, dashboard refresh and heartbeat monitors.' => 'Monitor de beschikbaarheid van sites en ontvang waarschuwingen via e-mail of Pushover. Beheer meerdere URL\'s vanuit WordPress-beheer met logging, JSON-import/-export, REST-ondersteuning, het verversen van het dashboard en heartbeat monitors.', 126 137 'Monitored URLs' => 'Gemonitorde URLs', … … 148 159 'Outbound client pings' => 'Uitgaande client pings', 149 160 'Overall status' => 'Algemene status', 161 'Overview' => 'Overzicht', 150 162 'Paste a previously exported JSON configuration here.' => 'Plak hier een eerder geëxporteerde JSON-configuratie.', 151 163 'Pause' => 'Pauzeren', … … 173 185 'Request Timeout (seconds)' => 'Aanvraagtime-out (seconden)', 174 186 'Request timeout' => 'Aanvraagtime-out', 187 'Response time' => 'Responstijd', 188 'Response time & average' => 'Responstijd & gemiddelde', 189 'Restore defaults' => 'Herstel standaard', 175 190 'Resume' => 'Hervatten', 176 191 'Retries:' => 'Pogingen:', … … 190 205 'Slower' => 'Langzamer', 191 206 'Stable' => 'Stabiel', 207 'Status' => 'Status', 192 208 'Status History' => 'Statusgeschiedenis', 209 'Status and monitor name always remain visible.' => 'Status en monitornaam blijven altijd zichtbaar.', 193 210 'Status becomes stale/down after roughly twice this interval without a ping.' => 'De status wordt stale/down na ongeveer twee keer dit interval zonder ping.', 194 211 'Store Pushover credentials here when file access to wp-config.php is not available.' => 'Sla Pushover-toegangsgegevens hier op wanneer bestandstoegang tot wp-config.php niet beschikbaar is.', 212 'System' => 'Systeem', 195 213 'The website %1$s is down. Error: %2$s.' => 'De site %1$s is down. Fout: %2$s.', 196 214 'The website %1$s is down. HTTP Status Code: %2$d.' => 'De site %1$s is down. HTTP-statuscode: %2$d.', … … 202 220 'Token:' => 'Token:', 203 221 'URL' => 'URL', 222 'URL / heartbeat interval' => 'URL / heartbeatinterval', 204 223 'URL added successfully!' => 'URL succesvol toegevoegd!', 205 224 'URL deleted successfully!' => 'URL succesvol verwijderd!', … … 214 233 'Updated just now.' => 'Zojuist geüpdatet.', 215 234 'Uptime' => 'Uptime', 235 'Uptime (24h, 7d, 30d)' => 'Uptime (24u, 7d, 30d)', 216 236 'Use comma-separated HTTP status codes or ranges. Default: 100-199,300-599.' => 'Gebruik komma-gescheiden HTTP-statuscodes of bereiken. Standaard: 100-199,300-599.', 217 237 'Use heartbeat monitors for computers, jobs, Home Assistant, NAS devices or internal apps that can reach this monitor through outbound HTTPS, but cannot be reached inbound.' => 'Gebruik heartbeat monitors voor computers, jobs, Home Assistant, NAS-apparaten of interne apps die deze monitor via uitgaande HTTPS kunnen bereiken, maar zelf niet van buitenaf bereikbaar zijn.', -
qndrs-availability-heartbeat-monitor/trunk/languages/qndrs-availability-heartbeat-monitor-nl_NL.po
r3666118 r3763178 1 1 msgid "" 2 2 msgstr "" 3 "Project-Id-Version: Qndrs Availability and Heartbeat Monitor 3. 6.1\n"3 "Project-Id-Version: Qndrs Availability and Heartbeat Monitor 3.7.1\n" 4 4 "Report-Msgid-Bugs-To: \n" 5 5 "PO-Revision-Date: 2026-08-26 12:00+0200\n" … … 985 985 msgid "Heartbeat monitor" 986 986 msgstr "Heartbeat monitor" 987 988 989 #: qndrs-availability-heartbeat-monitor.php:2253 990 msgid "Actions" 991 msgstr "Acties" 992 993 994 #: qndrs-availability-heartbeat-monitor.php:2372 995 msgid "Color theme" 996 msgstr "Kleurthema" 997 998 999 #: qndrs-availability-heartbeat-monitor.php:2376 1000 msgid "Columns" 1001 msgstr "Kolommen" 1002 1003 1004 #: qndrs-availability-heartbeat-monitor.php:2392 1005 msgid "Compact overview" 1006 msgstr "Compact overzicht" 1007 1008 1009 #: qndrs-availability-heartbeat-monitor.php:2374 1010 msgid "Dark" 1011 msgstr "Donker" 1012 1013 1014 #: qndrs-availability-heartbeat-monitor.php:2252 1015 msgid "Incidents" 1016 msgstr "Incidenten" 1017 1018 1019 #: qndrs-availability-heartbeat-monitor.php:2248 1020 msgid "Last check" 1021 msgstr "Laatste check" 1022 1023 1024 #: qndrs-availability-heartbeat-monitor.php:2380 1025 msgid "Last check & HTTP status" 1026 msgstr "Laatste check & HTTP-status" 1027 1028 1029 #: qndrs-availability-heartbeat-monitor.php:2373 1030 msgid "Light" 1031 msgstr "Licht" 1032 1033 1034 #: qndrs-availability-heartbeat-monitor.php:2247 1035 msgid "Monitor" 1036 msgstr "Monitor" 1037 1038 1039 #: qndrs-availability-heartbeat-monitor.php:2385 1040 msgid "Monitor type" 1041 msgstr "Type monitor" 1042 1043 1044 #: qndrs-availability-heartbeat-monitor.php:2249 1045 msgid "Response time" 1046 msgstr "Responstijd" 1047 1048 1049 #: qndrs-availability-heartbeat-monitor.php:2381 1050 msgid "Response time & average" 1051 msgstr "Responstijd & gemiddelde" 1052 1053 1054 #: qndrs-availability-heartbeat-monitor.php:2393 1055 msgid "Restore defaults" 1056 msgstr "Herstel standaard" 1057 1058 1059 #: qndrs-availability-heartbeat-monitor.php:2246 1060 msgid "Status" 1061 msgstr "Status" 1062 1063 1064 #: qndrs-availability-heartbeat-monitor.php:2377 1065 msgid "Status and monitor name always remain visible." 1066 msgstr "Status en monitornaam blijven altijd zichtbaar." 1067 1068 1069 #: qndrs-availability-heartbeat-monitor.php:2375 1070 msgid "System" 1071 msgstr "Systeem" 1072 1073 1074 #: qndrs-availability-heartbeat-monitor.php:2386 1075 msgid "URL / heartbeat interval" 1076 msgstr "URL / heartbeatinterval" 1077 1078 1079 #: qndrs-availability-heartbeat-monitor.php:2382 1080 msgid "Uptime (24h, 7d, 30d)" 1081 msgstr "Uptime (24u, 7d, 30d)" 1082 1083 #: qndrs-availability-heartbeat-monitor.php 1084 msgid "Overview" 1085 msgstr "Overzicht" -
qndrs-availability-heartbeat-monitor/trunk/qndrs-availability-heartbeat-monitor.php
r3666118 r3763178 4 4 * Plugin URI: https://github.com/qndrs/uptime-monitor 5 5 * Description: Monitor website availability and receive alerts by email or Pushover. Manage multiple URLs from the WordPress admin with logging, JSON import/export, REST support, dashboard refresh and heartbeat monitors. 6 * Version: 3. 6.16 * Version: 3.7.1 7 7 * Author: Robert E. Kuunders, GPT 8 8 * Author URI: https://qndrs.nl … … 34 34 class AvailabilityHeartbeatMonitor 35 35 { 36 public const VERSION = '3. 6.1';36 public const VERSION = '3.7.1'; 37 37 public const MAX_LOG_ENTRIES = 1000; 38 38 public const MAX_STATUS_HISTORY_PER_URL = 43200; … … 724 724 add_submenu_page( 725 725 'qndrs-availability-heartbeat-monitor', 726 __('Overview', 'qndrs-availability-heartbeat-monitor'), 727 __('Overview', 'qndrs-availability-heartbeat-monitor'), 728 'manage_options', 729 'qndrs-availability-heartbeat-monitor', 730 [$this, 'render_urls_page'] 731 ); 732 add_submenu_page( 733 'qndrs-availability-heartbeat-monitor', 726 734 __('Settings', 'qndrs-availability-heartbeat-monitor'), 727 735 __('Settings', 'qndrs-availability-heartbeat-monitor'), … … 775 783 'nonce' => wp_create_nonce('qndrs_ahm_nonce'), 776 784 'refresh_interval_ms' => 30000, 785 'view_storage_key' => 'qndrsAhmView:v1:' . get_current_blog_id() . ':' . get_current_user_id(), 777 786 ]); 778 787 // Localize script for translations … … 819 828 'refreshed' => __('Updated just now.', 'qndrs-availability-heartbeat-monitor'), 820 829 'auto_refresh' => __('Auto-refresh', 'qndrs-availability-heartbeat-monitor'), 830 'no_data' => __('No data', 'qndrs-availability-heartbeat-monitor'), 821 831 'heartbeat' => __('Heartbeat', 'qndrs-availability-heartbeat-monitor'), 822 832 'http_check' => __('HTTP check', 'qndrs-availability-heartbeat-monitor'), … … 1086 1096 $url_history = isset($history[$url_id]) && is_array($history[$url_id]) ? $history[$url_id] : []; 1087 1097 $url_history[] = $entry; 1088 $history[$url_id] = $this->prune_status_history($url_history); 1098 // Keep the verification snapshot intact until table storage is finalized. 1099 $history[$url_id] = $storage_mode === self::HISTORY_STORAGE_TABLE_SHADOW 1100 ? $url_history 1101 : $this->prune_status_history($url_history); 1089 1102 $this->update_status_history($history); 1090 1103 } … … 1500 1513 $paused = 0; 1501 1514 $degraded = 0; 1515 $unknown = 0; 1502 1516 $incident_count = 0; 1503 1517 $email_count = 0; … … 1519 1533 } elseif ($status === 'degraded') { 1520 1534 $degraded++; 1535 } else { 1536 $unknown++; 1521 1537 } 1522 1538 … … 1548 1564 $average_response = !empty($response_times) ? (int)round(array_sum($response_times) / count($response_times)) : null; 1549 1565 $average_uptime_24h = !empty($uptime_24h) ? round(array_sum($uptime_24h) / count($uptime_24h), 1) : null; 1550 $overall_status = $down > 0 ? 'incident' : ($degraded > 0 ? 'degraded' : 'operational');1566 $overall_status = $down > 0 ? 'incident' : ($degraded > 0 ? 'degraded' : ($unknown > 0 || $active === 0 ? 'unknown' : 'operational')); 1551 1567 $next_check = wp_next_scheduled(self::CRON_HOOK); 1552 1568 $average_response_display = __('No data', 'qndrs-availability-heartbeat-monitor'); … … 1564 1580 'paused' => $paused, 1565 1581 'degraded' => $degraded, 1582 'unknown' => $unknown, 1566 1583 'incidents' => $incident_count, 1567 1584 'email_count' => $email_count, … … 1578 1595 'overall_label' => $overall_status === 'incident' 1579 1596 ? __('Incident active', 'qndrs-availability-heartbeat-monitor') 1580 : ($overall_status === 'degraded' ? __('Degraded', 'qndrs-availability-heartbeat-monitor') : __('Operational', 'qndrs-availability-heartbeat-monitor')),1597 : ($overall_status === 'degraded' ? __('Degraded', 'qndrs-availability-heartbeat-monitor') : ($overall_status === 'unknown' ? __('Unknown', 'qndrs-availability-heartbeat-monitor') : __('Operational', 'qndrs-availability-heartbeat-monitor'))), 1581 1598 ]; 1582 1599 } … … 1727 1744 } 1728 1745 1729 $urls = $this->normalize_stored_urls();1730 $matched_index = $this->find_heartbeat_monitor_index_by_token($urls, $token);1731 1732 if ($matched_index === null) {1733 return new \WP_Error('qndrs_ahm_heartbeat_token_invalid', __('Heartbeat token is invalid.', 'qndrs-availability-heartbeat-monitor'), ['status' => 403]);1734 }1735 1736 $url_data = $urls[$matched_index];1737 if (isset($url_data['enabled']) && $url_data['enabled'] === false) {1738 return new \WP_Error('qndrs_ahm_heartbeat_disabled', __('Heartbeat monitor is disabled.', 'qndrs-availability-heartbeat-monitor'), ['status' => 403]);1739 }1740 1741 1746 $status = sanitize_key((string)$request->get_param('status')); 1742 1747 if (!in_array($status, ['up', 'down', 'error'], true)) { … … 1746 1751 $message = sanitize_text_field((string)$request->get_param('message')); 1747 1752 $timestamp = gmdate('Y-m-d H:i:s'); 1748 $url_data['last_seen'] = $timestamp; 1749 $url_data['last_status'] = $status; 1750 $url_data['last_message'] = $message; 1751 $urls[$matched_index] = $url_data; 1752 update_option('qndrs_ahm_urls', $urls); 1753 $url_data = $this->persist_heartbeat($token, $status, $message, $timestamp); 1754 if (is_wp_error($url_data)) { 1755 return $url_data; 1756 } 1753 1757 1754 1758 $history_message = $message !== '' ? $message : __('Heartbeat received.', 'qndrs-availability-heartbeat-monitor'); … … 1774 1778 'last_seen' => $timestamp, 1775 1779 ], 200); 1780 } 1781 1782 private function persist_heartbeat(string $token, string $status, string $message, string $timestamp) 1783 { 1784 global $wpdb; 1785 1786 // Bypass the option cache: each retry must merge with the latest committed list. 1787 for ($attempt = 0; $attempt < 5; $attempt++) { 1788 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- A compare-and-swap requires the exact current serialized option value. 1789 $stored_value = $wpdb->get_var($wpdb->prepare( 1790 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 1791 'qndrs_ahm_urls' 1792 )); 1793 $urls = $this->normalize_urls(maybe_unserialize($stored_value)); 1794 $index = $this->find_heartbeat_monitor_index_by_token($urls, $token); 1795 if ($index === null) { 1796 return new \WP_Error('qndrs_ahm_heartbeat_token_invalid', __('Heartbeat token is invalid.', 'qndrs-availability-heartbeat-monitor'), ['status' => 403]); 1797 } 1798 if (!$urls[$index]['enabled']) { 1799 return new \WP_Error('qndrs_ahm_heartbeat_disabled', __('Heartbeat monitor is disabled.', 'qndrs-availability-heartbeat-monitor'), ['status' => 403]); 1800 } 1801 1802 $urls[$index]['last_seen'] = $timestamp; 1803 $urls[$index]['last_status'] = $status; 1804 $urls[$index]['last_message'] = $message; 1805 $new_value = maybe_serialize($urls); 1806 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic comparison prevents concurrent heartbeat updates from overwriting each other; invalidate option caches after success. 1807 $updated = $wpdb->query($wpdb->prepare( 1808 "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND BINARY option_value = %s", 1809 $new_value, 1810 'qndrs_ahm_urls', 1811 $stored_value 1812 )); 1813 if ($updated === false) { 1814 break; 1815 } 1816 if ($updated === 1 || $new_value === $stored_value) { 1817 wp_cache_delete('qndrs_ahm_urls', 'options'); 1818 wp_cache_delete('alloptions', 'options'); 1819 return $urls[$index]; 1820 } 1821 } 1822 1823 return new \WP_Error('qndrs_ahm_heartbeat_write_failed', 'Heartbeat could not be saved. Please retry.', ['status' => 503]); 1776 1824 } 1777 1825 … … 1793 1841 'paused' => $dashboard['paused'], 1794 1842 'degraded' => $dashboard['degraded'], 1843 'unknown' => $dashboard['unknown'], 1795 1844 'incidents' => $dashboard['incidents'], 1796 1845 ], … … 2042 2091 private function render_dashboard_overview(array $dashboard): void 2043 2092 { 2044 $overall_status = isset($dashboard['overall_status']) ? sanitize_key($dashboard['overall_status']) : ' operational';2045 $status_class = $overall_status === 'incident' ? 'down' : ($overall_status === ' degraded' ? 'degraded' : 'up');2093 $overall_status = isset($dashboard['overall_status']) ? sanitize_key($dashboard['overall_status']) : 'unknown'; 2094 $status_class = $overall_status === 'incident' ? 'down' : ($overall_status === 'operational' ? 'up' : $overall_status); 2046 2095 2047 2096 echo '<section class="uptime-statusbar is-' . esc_attr($status_class) . '" aria-label="' . esc_attr__('Monitoring summary', 'qndrs-availability-heartbeat-monitor') . '">'; … … 2089 2138 private function render_dashboard_widget_content(array $dashboard): void 2090 2139 { 2091 $overall_status = isset($dashboard['overall_status']) ? sanitize_key($dashboard['overall_status']) : ' operational';2092 $status_class = $overall_status === 'incident' ? 'down' : ($overall_status === ' degraded' ? 'degraded' : 'up');2140 $overall_status = isset($dashboard['overall_status']) ? sanitize_key($dashboard['overall_status']) : 'unknown'; 2141 $status_class = $overall_status === 'incident' ? 'down' : ($overall_status === 'operational' ? 'up' : $overall_status); 2093 2142 $attention_urls = array_values(array_filter($dashboard['urls'], function (array $url_data): bool { 2094 2143 $url_dashboard = isset($url_data['dashboard']) && is_array($url_data['dashboard']) ? $url_data['dashboard'] : []; … … 2112 2161 echo '</div>'; 2113 2162 2114 if (empty($attention_urls) ) {2163 if (empty($attention_urls) && $overall_status === 'operational') { 2115 2164 echo '<p class="uptime-widget-ok">' . esc_html__('All monitored URLs are reachable.', 'qndrs-availability-heartbeat-monitor') . '</p>'; 2116 2165 } else { … … 2201 2250 private function render_url_dashboard_rows(array $urls): void 2202 2251 { 2252 echo '<div class="uptime-url-list-scroll' . (empty($urls) ? ' is-empty' : '') . '" tabindex="0" role="region" aria-label="' . esc_attr__('Monitored URLs', 'qndrs-availability-heartbeat-monitor') . '">'; 2253 echo '<div class="uptime-url-column-head" aria-hidden="true">'; 2254 echo '<span>' . esc_html__('Status', 'qndrs-availability-heartbeat-monitor') . '</span>'; 2255 echo '<span>' . esc_html__('Monitor', 'qndrs-availability-heartbeat-monitor') . '</span>'; 2256 echo '<span data-view-column="check">' . esc_html__('Last check', 'qndrs-availability-heartbeat-monitor') . '</span>'; 2257 echo '<span data-view-column="response">' . esc_html__('Response time', 'qndrs-availability-heartbeat-monitor') . '</span>'; 2258 echo '<span data-view-column="uptime">' . esc_html__('Uptime', 'qndrs-availability-heartbeat-monitor') . '</span>'; 2259 echo '<span data-view-column="alerts">' . esc_html__('Notifications', 'qndrs-availability-heartbeat-monitor') . '</span>'; 2260 echo '<span data-view-column="incident">' . esc_html__('Incidents', 'qndrs-availability-heartbeat-monitor') . '</span>'; 2261 echo '<span>' . esc_html__('Actions', 'qndrs-availability-heartbeat-monitor') . '</span></div>'; 2203 2262 echo '<section class="uptime-url-list" aria-label="' . esc_attr__('Monitored URLs', 'qndrs-availability-heartbeat-monitor') . '">'; 2204 2263 if (empty($urls)) { … … 2210 2269 } 2211 2270 echo '</section>'; 2271 echo '</div>'; 2212 2272 } 2213 2273 … … 2246 2306 echo '<div class="uptime-url-identity">'; 2247 2307 echo '<strong>' . esc_html($title) . '</strong>'; 2248 echo '<span class="uptime-chip uptime-chip-muted" >' . esc_html($type === 'heartbeat' ? __('Heartbeat', 'qndrs-availability-heartbeat-monitor') : __('HTTP check', 'qndrs-availability-heartbeat-monitor')) . '</span>';2308 echo '<span class="uptime-chip uptime-chip-muted" data-view-column="type">' . esc_html($type === 'heartbeat' ? __('Heartbeat', 'qndrs-availability-heartbeat-monitor') : __('HTTP check', 'qndrs-availability-heartbeat-monitor')) . '</span>'; 2249 2309 if ($type !== 'heartbeat' && $url !== '') { 2250 echo '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>';2310 echo '<a data-view-column="address" href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($url) . '</a>'; 2251 2311 } elseif ($type === 'heartbeat' && !empty($url_data['expected_interval'])) { 2252 2312 /* translators: %d: Expected heartbeat interval in seconds. */ 2253 echo '<span >' . esc_html(sprintf(__('Expected every %d seconds', 'qndrs-availability-heartbeat-monitor'), absint($url_data['expected_interval']))) . '</span>';2254 } 2255 echo '</div>'; 2256 2257 echo '<div class="uptime-url-current" >';2313 echo '<span data-view-column="address">' . esc_html(sprintf(__('Expected every %d seconds', 'qndrs-availability-heartbeat-monitor'), absint($url_data['expected_interval']))) . '</span>'; 2314 } 2315 echo '</div>'; 2316 2317 echo '<div class="uptime-url-current" data-view-column="check">'; 2258 2318 if ($latest !== null) { 2259 2319 $status_code = isset($latest['status_code']) && $latest['status_code'] !== null ? absint($latest['status_code']) : 0; 2260 $response_time_ms = isset($latest['response_time_ms']) && $latest['response_time_ms'] !== null ? absint($latest['response_time_ms']) : null;2261 2320 $timestamp = isset($latest['timestamp']) ? (string)$latest['timestamp'] : ''; 2262 2321 $display_timestamp = isset($latest['display_timestamp']) ? (string)$latest['display_timestamp'] : $timestamp; … … 2266 2325 echo '<span class="uptime-chip">' . esc_html(sprintf(__('HTTP %d', 'qndrs-availability-heartbeat-monitor'), $status_code)) . '</span>'; 2267 2326 } 2268 if ($response_time_ms !== null) {2269 /* translators: %d: Response time in milliseconds. */2270 echo '<span class="uptime-chip">' . esc_html(sprintf(__('%d ms', 'qndrs-availability-heartbeat-monitor'), $response_time_ms)) . '</span>';2271 }2272 2327 if ($timestamp !== '') { 2273 2328 echo '<time class="uptime-chip uptime-chip-muted" datetime="' . esc_attr($timestamp) . '">' . esc_html($display_timestamp) . '</time>'; … … 2278 2333 echo '</div>'; 2279 2334 2280 echo '<div class="uptime-url-metrics">'; 2281 $this->render_uptime_summary($uptime); 2335 echo '<div class="uptime-url-response" data-view-column="response">'; 2336 if ($latest !== null && isset($latest['response_time_ms'])) { 2337 /* translators: %d: Response time in milliseconds. */ 2338 echo '<span class="uptime-chip">' . esc_html(sprintf(__('%d ms', 'qndrs-availability-heartbeat-monitor'), absint($latest['response_time_ms']))) . '</span>'; 2339 } elseif (empty($dashboard['average_response_time_display'])) { 2340 echo '<span class="uptime-chip uptime-chip-muted">' . esc_html__('No data', 'qndrs-availability-heartbeat-monitor') . '</span>'; 2341 } 2282 2342 if (!empty($dashboard['average_response_time_display'])) { 2283 2343 echo '<span class="uptime-response-average">' . esc_html($dashboard['average_response_time_display']) . '</span>'; … … 2285 2345 echo '</div>'; 2286 2346 2287 echo '<div class="uptime-url-alerts">'; 2347 echo '<div class="uptime-url-metrics" data-view-column="uptime">'; 2348 $this->render_uptime_summary($uptime); 2349 echo '</div>'; 2350 2351 echo '<div class="uptime-url-alerts" data-view-column="alerts">'; 2288 2352 echo '<span class="uptime-chip' . ($notifications_label === __('No alerts', 'qndrs-availability-heartbeat-monitor') ? ' is-warning' : '') . '">' . esc_html($notifications_label) . '</span>'; 2353 echo '</div>'; 2354 echo '<div class="uptime-url-incident" data-view-column="incident">'; 2289 2355 echo '<span class="uptime-chip' . ($incident_open ? ' is-danger' : ' uptime-chip-muted') . '">' . esc_html($incident_label) . '</span>'; 2290 2356 if ($incident_duration_display !== '') { … … 2305 2371 } 2306 2372 2307 /** 2308 * Renders the admin page for managing monitored URLs. 2309 */ 2373 /** Renders browser-local overview preferences; no configuration is submitted. */ 2374 private function render_view_controls(): void 2375 { 2376 echo '<div class="uptime-view-controls" hidden>'; 2377 echo '<div class="uptime-theme-switch" role="group" aria-label="' . esc_attr__('Color theme', 'qndrs-availability-heartbeat-monitor') . '">'; 2378 echo '<button type="button" data-theme-choice="light" aria-pressed="true">' . esc_html__('Light', 'qndrs-availability-heartbeat-monitor') . '</button>'; 2379 echo '<button type="button" data-theme-choice="dark" aria-pressed="false">' . esc_html__('Dark', 'qndrs-availability-heartbeat-monitor') . '</button>'; 2380 echo '<button type="button" data-theme-choice="system" aria-pressed="false">' . esc_html__('System', 'qndrs-availability-heartbeat-monitor') . '</button></div>'; 2381 echo '<details class="uptime-column-settings"><summary class="button">' . esc_html__('Columns', 'qndrs-availability-heartbeat-monitor') . ' <span class="uptime-column-count" aria-live="polite" aria-atomic="true">7/7</span></summary>'; 2382 echo '<div class="uptime-column-panel"><p>' . esc_html__('Status and monitor name always remain visible.', 'qndrs-availability-heartbeat-monitor') . '</p>'; 2383 echo '<fieldset><legend class="screen-reader-text">' . esc_html__('Columns', 'qndrs-availability-heartbeat-monitor') . '</legend>'; 2384 $columns = [ 2385 'check' => __('Last check & HTTP status', 'qndrs-availability-heartbeat-monitor'), 2386 'response' => __('Response time & average', 'qndrs-availability-heartbeat-monitor'), 2387 'uptime' => __('Uptime (24h, 7d, 30d)', 'qndrs-availability-heartbeat-monitor'), 2388 'alerts' => __('Notifications', 'qndrs-availability-heartbeat-monitor'), 2389 'incident' => __('Incidents', 'qndrs-availability-heartbeat-monitor'), 2390 'type' => __('Monitor type', 'qndrs-availability-heartbeat-monitor'), 2391 'address' => __('URL / heartbeat interval', 'qndrs-availability-heartbeat-monitor'), 2392 ]; 2393 foreach ($columns as $key => $label) { 2394 echo '<label><input type="checkbox" data-view-toggle="' . esc_attr($key) . '" checked> ' . esc_html($label) . '</label>'; 2395 } 2396 echo '</fieldset><div class="uptime-column-panel-actions">'; 2397 echo '<button type="button" class="button uptime-view-compact">' . esc_html__('Compact overview', 'qndrs-availability-heartbeat-monitor') . '</button>'; 2398 echo '<button type="button" class="button uptime-view-reset">' . esc_html__('Restore defaults', 'qndrs-availability-heartbeat-monitor') . '</button></div></div></details></div>'; 2399 } 2400 2401 /** Renders the admin page for managing monitored URLs. */ 2310 2402 public function render_urls_page(): void 2311 2403 { … … 2374 2466 $urls = $dashboard['urls']; 2375 2467 2376 echo '<div class="wrap uptime-monitor-admin uptime-monitor-dashboard ">';2468 echo '<div class="wrap uptime-monitor-admin uptime-monitor-dashboard uptime-monitor-overview">'; 2377 2469 echo '<div class="uptime-dashboard-header">'; 2378 2470 echo '<h1>' . esc_html__('Qndrs Monitor', 'qndrs-availability-heartbeat-monitor') . '</h1>'; … … 2383 2475 echo '</div>'; 2384 2476 echo '</div>'; 2477 $this->render_view_controls(); 2385 2478 echo '<div class="uptime-monitor-notices" aria-live="polite" aria-atomic="true"></div>'; 2386 2479 … … 3181 3274 global $wpdb; 3182 3275 3276 if ($this->get_history_storage_mode() === self::HISTORY_STORAGE_TABLE) { 3277 return ['success' => false, 'message' => 'History storage is already finalized; refusing to replace table history.']; 3278 } 3279 3183 3280 $batch_size = min(2000, max(50, $batch_size)); 3184 3281 $this->create_history_table(); … … 3186 3283 update_option('qndrs_ahm_data_schema_version', self::DATA_SCHEMA_VERSION, false); 3187 3284 $history = $this->get_status_history(); 3285 // Legacy is authoritative until verification completes. Rebuild the shadow 3286 // copy so retention, rollback and interrupted imports cannot leave extra rows. 3287 update_option('qndrs_ahm_history_storage', self::HISTORY_STORAGE_LEGACY, false); 3288 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared -- Only the unfinalized shadow copy is cleared; legacy history is retained for recovery. 3289 if ($wpdb->query('DELETE FROM ' . $this->get_history_table_name()) === false) { 3290 return ['success' => false, 'message' => 'Could not reset shadow history; legacy storage remains active.']; 3291 } 3188 3292 $batch = []; 3189 3293 $valid_entries = 0; -
qndrs-availability-heartbeat-monitor/trunk/readme.txt
r3666118 r3763178 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.0 7 Stable tag: 3. 6.17 Stable tag: 3.7.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 15 15 Qndrs Availability and Heartbeat Monitor checks external URLs from the WordPress dashboard and can also monitor firewalled clients through outbound heartbeat pings. It supports multiple monitored URLs, per-URL monitoring toggles, email alerts, configurable Pushover alerts, status history, response times, uptime percentages, JSON import/export, structured logging, heartbeat monitors, and authenticated REST endpoints. 16 16 17 The monitor overview supports light, dark and system themes, configurable columns and a compact view. Display preferences are remembered in this browser for each site and WordPress user, including during automatic refresh. Status, monitor names and management actions remain available. 18 17 19 == Installation == 18 20 19 21 1. Upload the plugin folder to `/wp-content/plugins/qndrs-availability-heartbeat-monitor`. 20 22 2. Activate Qndrs Availability and Heartbeat Monitor from the WordPress plugins page. 21 3. Open Qndrs Monitor in the WordPress admin menu.23 3. Open Qndrs Monitor > Overview in the WordPress admin menu. Use Qndrs Monitor > Settings for configuration. 22 24 4. Add one or more URLs and choose notification channels. 23 25 … … 69 71 70 72 == Changelog == 73 74 = 3.7.1 = 75 76 * Renamed the first submenu to Overview to distinguish it from the Qndrs Monitor parent menu; Settings remains the configuration page. 77 78 = 3.7.0 = 79 80 * Added light, dark and system themes with configurable overview columns and a compact view. 81 * Preserved display preferences and expanded history during automatic refresh. 82 * Fixed shadow-history migration recovery, unknown aggregate status and concurrent heartbeat persistence. 83 * Added Dutch translations for the new display controls. 71 84 72 85 = 3.6.1 = -
qndrs-availability-heartbeat-monitor/trunk/uptime-monitor.css
r3608284 r3763178 18 18 } 19 19 20 /* Overview preferences are deliberately scoped away from settings and the widget. */ 21 .uptime-monitor-overview { 22 --ump-columns: 90px minmax(190px, 1.4fr) minmax(150px, .85fr) minmax(90px, .5fr) minmax(190px, 1fr) minmax(100px, .6fr) minmax(100px, .6fr) 340px; 23 --ump-list-min-width: 1386px; 24 --ump-soft: #f6f7f7; 25 --ump-link: #2271b1; 26 --ump-shadow: 0 12px 34px rgba(0, 0, 0, .16); 27 } 28 29 .uptime-monitor-overview [hidden] { display: none !important; } 30 .uptime-view-controls { align-items: center; display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end; margin: 12px 0; } 31 .uptime-theme-switch { background: var(--ump-panel); border: 1px solid var(--ump-border); border-radius: 6px; display: flex; padding: 3px; } 32 .uptime-theme-switch button { background: transparent; border: 0; border-radius: 3px; color: var(--ump-muted); cursor: pointer; font: inherit; padding: 6px 12px; } 33 .uptime-theme-switch button[aria-pressed="true"] { background: var(--ump-soft); color: var(--ump-text); box-shadow: 0 1px 3px rgba(0, 0, 0, .15); } 34 .uptime-monitor-overview :is(button, summary, input, a):focus-visible { outline: 2px solid var(--ump-link); outline-offset: 2px; } 35 .uptime-column-settings { position: relative; } 36 .uptime-column-settings > summary { cursor: pointer; list-style: none; } 37 .uptime-column-settings > summary::-webkit-details-marker { display: none; } 38 .uptime-column-settings[open] > summary { border-color: var(--ump-link); } 39 .uptime-column-count { border-left: 1px solid var(--ump-border); color: var(--ump-muted); font-size: 11px; margin-left: 6px; padding-left: 8px; } 40 .uptime-column-panel { background: var(--ump-panel); border: 1px solid var(--ump-border); border-radius: 6px; box-shadow: var(--ump-shadow); box-sizing: border-box; max-height: 65vh; overflow-y: auto; padding: 16px; position: absolute; right: 0; top: calc(100% + 8px); width: 310px; z-index: 20; } 41 .uptime-column-panel p { color: var(--ump-muted); font-size: 12px; margin: 0 0 10px; } 42 .uptime-column-panel fieldset { border: 0; margin: 0; min-width: 0; padding: 0; } 43 .uptime-column-panel label { align-items: center; cursor: pointer; display: flex; gap: 9px; padding: 7px 0; } 44 .uptime-column-panel input[type="checkbox"] { flex: 0 0 auto; margin: 0; } 45 .uptime-column-panel-actions { border-top: 1px solid var(--ump-border); display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; padding-top: 12px; } 46 .uptime-column-panel-actions .button { flex: 1 1 auto; } 47 .uptime-url-list-scroll { overflow-x: auto; padding: 2px 2px 8px; scrollbar-color: var(--ump-border) transparent; } 48 .uptime-url-column-head { align-items: center; box-sizing: border-box; color: var(--ump-muted); display: grid; font-size: 11px; font-weight: 600; gap: 12px 14px; grid-template-columns: var(--ump-columns); min-width: var(--ump-list-min-width); padding: 8px 17px 8px 21px; } 49 .uptime-monitor-overview .uptime-url-list { min-width: var(--ump-list-min-width); } 50 .uptime-url-list-scroll.is-empty .uptime-url-column-head { display: none; } 51 .uptime-monitor-overview .uptime-url-list-scroll.is-empty .uptime-url-list { min-width: 0; } 52 .uptime-monitor-overview .uptime-url-row { gap: 12px 14px; grid-template-columns: var(--ump-columns); } 53 .uptime-monitor-overview .uptime-url-actions { justify-content: flex-end; } 54 .uptime-url-response, .uptime-url-incident { align-items: center; display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; } 55 .uptime-hide-check [data-view-column="check"], 56 .uptime-hide-response [data-view-column="response"], 57 .uptime-hide-uptime [data-view-column="uptime"], 58 .uptime-hide-alerts [data-view-column="alerts"], 59 .uptime-hide-incident [data-view-column="incident"], 60 .uptime-hide-type [data-view-column="type"], 61 .uptime-hide-address [data-view-column="address"] { display: none !important; } 62 63 .uptime-monitor-overview[data-theme="dark"] { 64 --ump-bg: #10161f; 65 --ump-panel: #19222e; 66 --ump-soft: #202c3a; 67 --ump-border: #344152; 68 --ump-text: #e7edf5; 69 --ump-muted: #a9b6c7; 70 --ump-up: #6ad79a; 71 --ump-down: #ff9d9d; 72 --ump-paused: #a9b6c7; 73 --ump-warning: #f0c36d; 74 --ump-info: #9bbcff; 75 --ump-link: #9bbcff; 76 --ump-shadow: 0 12px 34px rgba(0, 0, 0, .4); 77 background: var(--ump-bg); 78 color-scheme: dark; 79 } 80 body.qndrs-ahm-dark-overview #wpcontent, 81 body.qndrs-ahm-dark-overview #wpfooter { background: #10161f; } 82 body.qndrs-ahm-dark-overview #wpfooter { color: #a9b6c7; } 83 body.qndrs-ahm-dark-overview #wpfooter a { color: #9bbcff; } 84 .uptime-monitor-overview[data-theme="dark"] :is(h1, h2, h3, label) { color: var(--ump-text); } 85 .uptime-monitor-overview[data-theme="dark"] a { color: var(--ump-link); } 86 .uptime-monitor-overview[data-theme="dark"] .button { background: var(--ump-panel); border-color: var(--ump-border); color: var(--ump-link); } 87 .uptime-monitor-overview[data-theme="dark"] .button:hover { background: var(--ump-soft); border-color: var(--ump-link); } 88 .uptime-monitor-overview[data-theme="dark"] .button-primary { background: #5269ee; border-color: #5269ee; color: #fff; } 89 .uptime-monitor-overview[data-theme="dark"] .button-primary:hover { background: #4359d1; border-color: #4359d1; } 90 .uptime-monitor-overview[data-theme="dark"] .button:disabled { background: var(--ump-soft) !important; border-color: var(--ump-border) !important; color: var(--ump-muted) !important; } 91 .uptime-monitor-overview[data-theme="dark"] :is(input[type="url"], input[type="text"], input[type="checkbox"]) { background: var(--ump-bg); border-color: var(--ump-muted); color: var(--ump-text); } 92 .uptime-monitor-overview[data-theme="dark"] input::placeholder { color: var(--ump-muted); } 93 .uptime-monitor-overview[data-theme="dark"] .uptime-chip { background: var(--ump-soft); border-color: var(--ump-border); color: var(--ump-text); } 94 .uptime-monitor-overview[data-theme="dark"] .uptime-chip-muted { color: var(--ump-muted); } 95 .uptime-monitor-overview[data-theme="dark"] .uptime-chip.is-warning { background: #382e1d; border-color: #706044; color: var(--ump-warning); } 96 .uptime-monitor-overview[data-theme="dark"] .uptime-chip.is-danger { background: #382329; border-color: #70404b; color: var(--ump-down); } 97 .uptime-monitor-overview[data-theme="dark"] .uptime-percentage { background: var(--ump-soft); border-color: var(--ump-border); } 98 .uptime-monitor-overview[data-theme="dark"] .uptime-percentage-label { background: #2b3747; color: var(--ump-muted); } 99 .uptime-monitor-overview[data-theme="dark"] .uptime-percentage-value { color: var(--ump-up); } 100 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-response-average, .uptime-history-response-time) { background: #20364a; color: #acdcff; } 101 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-history-empty, .uptime-history-time, .uptime-history-message) { color: var(--ump-muted); } 102 .uptime-monitor-overview[data-theme="dark"] .uptime-history-code { color: var(--ump-text); } 103 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-status-up, .uptime-response-trend-faster) { background: #213b30; color: var(--ump-up); } 104 .uptime-monitor-overview[data-theme="dark"] .uptime-status-down { background: #44282f; color: var(--ump-down); } 105 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-status-error, .uptime-status-degraded, .uptime-response-trend-slower) { background: #382e1d; color: var(--ump-warning); } 106 .uptime-monitor-overview[data-theme="dark"] :is(.uptime-status-paused, .uptime-status-unknown, .uptime-response-trend-stable) { background: var(--ump-soft); color: var(--ump-muted); } 107 .uptime-monitor-overview[data-theme="dark"] .uptime-delete-button { border-color: #70404b; color: var(--ump-down); } 108 .uptime-monitor-overview[data-theme="dark"] .uptime-delete-button:hover { background: #44282f; border-color: var(--ump-down); } 109 .uptime-monitor-overview[data-theme="dark"] .uptime-monitor-notice { background: var(--ump-panel); color: var(--ump-text); } 110 .uptime-monitor-overview[data-theme="dark"] .notice-dismiss::before { color: var(--ump-muted); } 111 112 @media (max-width: 782px) { 113 .uptime-monitor-overview .uptime-dashboard-header { align-items: flex-start; flex-direction: column; } 114 .uptime-view-controls { justify-content: space-between; } 115 .uptime-column-settings { margin-left: auto; } 116 .uptime-column-panel { max-width: calc(100vw - 40px); } 117 } 118 20 119 .uptime-widget-refresh-meter { 21 120 background: rgba(34, 113, 177, 0.12); … … 62 161 border-left-color: var(--ump-warning); 63 162 background: rgba(183, 121, 31, 0.07); 163 } 164 165 .uptime-dashboard-widget.is-unknown .uptime-widget-status { 166 border-left-color: var(--ump-paused); 64 167 } 65 168 … … 219 322 .uptime-statusbar.is-degraded { 220 323 border-left-color: var(--ump-warning); 324 } 325 326 .uptime-statusbar.is-unknown { 327 border-left-color: var(--ump-paused); 221 328 } 222 329 -
qndrs-availability-heartbeat-monitor/trunk/uptime-monitor.js
r3651690 r3763178 6 6 let widgetRefreshTimer = null; 7 7 let widgetRefreshRequest = null; 8 const overview = $('.uptime-monitor-overview'); 9 const viewPreferences = { theme: 'light', hidden: [] }; 10 const viewColumns = { 11 check: { track: 'minmax(150px, .85fr)', width: 164 }, 12 response: { track: 'minmax(90px, .5fr)', width: 104 }, 13 uptime: { track: 'minmax(190px, 1fr)', width: 204 }, 14 alerts: { track: 'minmax(100px, .6fr)', width: 114 }, 15 incident: { track: 'minmax(100px, .6fr)', width: 114 } 16 }; 17 const optionalViewFields = [...Object.keys(viewColumns), 'type', 'address']; 18 const systemTheme = window.matchMedia('(prefers-color-scheme: dark)'); 19 20 function applyViewTheme() { 21 if (!overview.length) { 22 return; 23 } 24 const theme = viewPreferences.theme === 'system' ? (systemTheme.matches ? 'dark' : 'light') : viewPreferences.theme; 25 overview.attr('data-theme', theme); 26 $('body').toggleClass('qndrs-ahm-dark-overview', theme === 'dark'); 27 overview.find('[data-theme-choice]').each(function () { 28 $(this).attr('aria-pressed', String(this.dataset.themeChoice === viewPreferences.theme)); 29 }); 30 } 31 32 function applyViewColumns() { 33 if (!overview.length) { 34 return; 35 } 36 const visible = Object.keys(viewColumns).filter(key => !viewPreferences.hidden.includes(key)); 37 overview[0].style.setProperty('--ump-columns', [ 38 '90px', 'minmax(190px, 1.4fr)', ...visible.map(key => viewColumns[key].track), '340px' 39 ].join(' ')); 40 overview[0].style.setProperty('--ump-list-min-width', (686 + visible.reduce((width, key) => width + viewColumns[key].width, 0)) + 'px'); 41 optionalViewFields.forEach(function (key) { 42 const hidden = viewPreferences.hidden.includes(key); 43 overview.toggleClass('uptime-hide-' + key, hidden); 44 overview.find('[data-view-toggle="' + key + '"]').prop('checked', !hidden); 45 }); 46 overview.find('.uptime-column-count').text((optionalViewFields.length - viewPreferences.hidden.length) + '/' + optionalViewFields.length); 47 } 48 49 function saveViewPreferences() { 50 try { 51 window.localStorage.setItem(qndrsAhmAjax.view_storage_key, JSON.stringify(viewPreferences)); 52 } catch (error) { 53 // Preferences still work for this page when browser storage is unavailable. 54 } 55 } 56 57 function initializeViewPreferences() { 58 if (!overview.length) { 59 return; 60 } 61 try { 62 const saved = JSON.parse(window.localStorage.getItem(qndrsAhmAjax.view_storage_key)); 63 if (saved && ['light', 'dark', 'system'].includes(saved.theme)) { 64 viewPreferences.theme = saved.theme; 65 } 66 if (saved && Array.isArray(saved.hidden)) { 67 viewPreferences.hidden = optionalViewFields.filter(key => saved.hidden.includes(key)); 68 } 69 } catch (error) { 70 // Corrupt or blocked storage must not prevent monitoring controls from working. 71 } 72 applyViewTheme(); 73 applyViewColumns(); 74 overview.find('.uptime-view-controls').prop('hidden', false); 75 overview.on('click', '[data-theme-choice]', function () { 76 viewPreferences.theme = this.dataset.themeChoice; 77 applyViewTheme(); 78 saveViewPreferences(); 79 }); 80 overview.on('change', '[data-view-toggle]', function () { 81 viewPreferences.hidden = optionalViewFields.filter(key => !overview.find('[data-view-toggle="' + key + '"]').prop('checked')); 82 applyViewColumns(); 83 saveViewPreferences(); 84 }); 85 overview.on('click', '.uptime-view-compact, .uptime-view-reset', function () { 86 viewPreferences.hidden = $(this).hasClass('uptime-view-compact') ? ['check', 'alerts', 'incident', 'type', 'address'] : []; 87 applyViewColumns(); 88 saveViewPreferences(); 89 }); 90 $(document).on('click.qndrsAhmView', function (event) { 91 const settings = overview.find('.uptime-column-settings'); 92 if (!settings[0].contains(event.target)) { 93 settings.prop('open', false); 94 } 95 }).on('keydown.qndrsAhmView', function (event) { 96 const settings = overview.find('.uptime-column-settings'); 97 if (event.key === 'Escape' && settings.prop('open')) { 98 settings.prop('open', false).find('summary').trigger('focus'); 99 } 100 }); 101 systemTheme.addEventListener('change', applyViewTheme); 102 } 103 104 initializeViewPreferences(); 8 105 9 106 function escapeHtml(value) { … … 205 302 206 303 const statusCode = parseInt(latest.status_code, 10); 207 const responseTime = parseInt(latest.response_time_ms, 10);208 304 const timestamp = latest.timestamp || ''; 209 305 const displayTimestamp = latest.display_timestamp || timestamp; … … 211 307 return ` 212 308 ${statusCode ? `<span class="uptime-chip">${escapeHtml(qndrsAhmL10n.http_status.replace('%d', statusCode))}</span>` : ''} 213 ${!isNaN(responseTime) ? `<span class="uptime-chip">${escapeHtml(qndrsAhmL10n.response_time_ms.replace('%d', responseTime))}</span>` : ''}214 309 ${timestamp ? `<time class="uptime-chip uptime-chip-muted" datetime="${escapeHtml(timestamp)}">${escapeHtml(displayTimestamp)}</time>` : ''} 215 310 `; … … 229 324 const title = type === 'heartbeat' ? (urlData.name || qndrsAhmL10n.heartbeat) : getHost(url); 230 325 const identityMeta = type === 'heartbeat' 231 ? `<span >${escapeHtml((qndrsAhmL10n.expected_every_seconds || 'Expected every %d seconds').replace('%d', parseInt(urlData.heartbeat && urlData.heartbeat.expected_interval ? urlData.heartbeat.expected_interval : urlData.expected_interval, 10) || 300))}</span>`232 : `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`;326 ? `<span data-view-column="address">${escapeHtml((qndrsAhmL10n.expected_every_seconds || 'Expected every %d seconds').replace('%d', parseInt(urlData.heartbeat && urlData.heartbeat.expected_interval ? urlData.heartbeat.expected_interval : urlData.expected_interval, 10) || 300))}</span>` 327 : `<a data-view-column="address" href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`; 233 328 const averageResponse = dashboard.average_response_time_display 234 329 ? `<span class="uptime-response-average">${escapeHtml(dashboard.average_response_time_display)}</span>` 235 330 : ''; 331 const latest = dashboard.latest || {}; 332 const responseTime = parseInt(latest.response_time_ms, 10); 333 const currentResponse = !isNaN(responseTime) 334 ? `<span class="uptime-chip">${escapeHtml(qndrsAhmL10n.response_time_ms.replace('%d', responseTime))}</span>` 335 : (averageResponse ? '' : `<span class="uptime-chip uptime-chip-muted">${escapeHtml(qndrsAhmL10n.no_data)}</span>`); 236 336 237 337 return ` … … 243 343 <div class="uptime-url-identity"> 244 344 <strong>${escapeHtml(title)}</strong> 245 <span class="uptime-chip uptime-chip-muted" >${escapeHtml(type === 'heartbeat' ? qndrsAhmL10n.heartbeat : qndrsAhmL10n.http_check)}</span>345 <span class="uptime-chip uptime-chip-muted" data-view-column="type">${escapeHtml(type === 'heartbeat' ? qndrsAhmL10n.heartbeat : qndrsAhmL10n.http_check)}</span> 246 346 ${identityMeta} 247 347 </div> 248 <div class="uptime-url-current" >348 <div class="uptime-url-current" data-view-column="check"> 249 349 ${renderCurrentCheck(dashboard)} 250 350 </div> 251 <div class="uptime-url-metrics"> 351 <div class="uptime-url-response" data-view-column="response"> 352 ${currentResponse} 353 ${averageResponse} 354 </div> 355 <div class="uptime-url-metrics" data-view-column="uptime"> 252 356 ${renderUptimeSummary(urlData.uptime)} 253 ${averageResponse} 254 </div> 255 <div class="uptime-url-alerts"> 357 </div> 358 <div class="uptime-url-alerts" data-view-column="alerts"> 256 359 <span class="uptime-chip${notificationsLabel === qndrsAhmL10n.no_alerts ? ' is-warning' : ''}">${escapeHtml(notificationsLabel)}</span> 360 </div> 361 <div class="uptime-url-incident" data-view-column="incident"> 257 362 <span class="uptime-chip${incidentOpen ? ' is-danger' : ' uptime-chip-muted'}">${escapeHtml(incidentLabel)}</span> 258 363 ${incidentDuration ? `<span class="uptime-chip is-danger">${escapeHtml(incidentDuration)}</span>` : ''} … … 272 377 function updateUrlList(urls) { 273 378 const list = $('.uptime-url-list'); 379 list.closest('.uptime-url-list-scroll').toggleClass('is-empty', !Array.isArray(urls) || urls.length === 0); 274 380 const expandedIds = []; 275 381 … … 466 572 467 573 $('.uptime-auto-refresh-input').prop('checked', isEnabled); 468 window.localStorage.setItem(autoRefreshStorageKey, isEnabled ? '1' : '0'); 574 try { 575 window.localStorage.setItem(autoRefreshStorageKey, isEnabled ? '1' : '0'); 576 } catch (error) { 577 // Auto-refresh remains usable without persistent browser storage. 578 } 469 579 470 580 if (isEnabled) { … … 511 621 }); 512 622 513 if ($('.uptime-auto-refresh-input').length && window.localStorage.getItem(autoRefreshStorageKey) === '1') { 514 setAutoRefresh(true); 623 try { 624 if ($('.uptime-auto-refresh-input').length && window.localStorage.getItem(autoRefreshStorageKey) === '1') { 625 setAutoRefresh(true); 626 } 627 } catch (error) { 628 // Keep manual refresh and all other controls available. 515 629 } 516 630
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)