diff --git a/README.md b/README.md index 89c654b..dc3f3fa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AssetManager 0.5.5.30 +# AssetManager 0.5.5.31 AssetManager is a self-hosted web application for managing IT equipment and other organizational assets. The project is released under the **Apache License 2.0** and may be used, modified, and redistributed for private and commercial purposes. diff --git a/VERSION b/VERSION index daf6f6e..f13c9f2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.5.30 +0.5.5.31 diff --git a/app/i18n.py b/app/i18n.py index 700b616..c566bc0 100644 --- a/app/i18n.py +++ b/app/i18n.py @@ -799,6 +799,10 @@ BASE_TRANSLATIONS.update({ "software.version_count": ("Number of versions", "Anzahl Versionen"), "software.asset_count": ("Assets", "Assets"), "software.entry_count": ("Entries", "Einträge"), + "software.unique_asset_count": ("Unique assets", "Eindeutige Assets"), + "software.unique_asset_count_help": ("Number of different assets on which this grouped software was found. An asset is counted only once, even if multiple versions or matching inventory records exist.", "Anzahl unterschiedlicher Assets, auf denen diese gruppierte Software gefunden wurde. Ein Asset wird nur einmal gezählt, auch wenn mehrere Versionen oder passende Inventareinträge vorhanden sind."), + "software.inventory_entry_count": ("Inventory entries", "Inventareinträge"), + "software.inventory_entry_count_help": ("Number of matching software inventory records. This value can be higher than the number of unique assets when multiple versions or records exist on the same asset.", "Anzahl der passenden Software-Inventardatensätze. Dieser Wert kann höher als die Anzahl eindeutiger Assets sein, wenn auf einem Asset mehrere Versionen oder Einträge vorhanden sind."), "software.no_global_inventory": ("No central software inventory is available yet.", "Noch keine zentrale Softwareinventur vorhanden."), "software.last_job": ("Last job", "Letzter Job"), "software.last_inventory": ("Last inventory", "Letzte Inventur") diff --git a/app/static/css/app.css b/app/static/css/app.css index aecb3ee..32fce9a 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -2823,3 +2823,6 @@ html[data-theme="dark"] .chart-total{ /* Administrator editor for the protected MeshCentral node ID. */ .mesh-node-id-editor{display:grid;gap:.55rem}.mesh-node-id-input{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.mesh-node-id-actions{display:flex;flex-wrap:wrap;gap:.55rem;align-items:center}.mesh-node-id-actions .button{width:auto;margin:0}.mesh-node-id-editor small{font-weight:400;margin:0} + +/* Keep server-filter controls visible while only the table body is refreshed. */ +table.server-filter-loading tbody{opacity:.55;transition:opacity .12s ease} diff --git a/app/static/js/software-job-selection.js b/app/static/js/software-job-selection.js index 5c89a0c..f7dfe47 100644 --- a/app/static/js/software-job-selection.js +++ b/app/static/js/software-job-selection.js @@ -63,6 +63,9 @@ if (!event.detail?.table || event.detail.table === table) update(); }); window.addEventListener('software-job-status-changed', update); + window.addEventListener('asset-table-content-reloaded', event => { + if (event.detail?.table === table) update(); + }); update(); } diff --git a/app/static/js/software-job-status.js b/app/static/js/software-job-status.js index 81eab65..69b4536 100644 --- a/app/static/js/software-job-status.js +++ b/app/static/js/software-job-status.js @@ -69,10 +69,13 @@ } let timer = null; + let requestInProgress = false; async function refresh() { + if (requestInProgress) return; const active = activeRows(); if (!active.length) return; const ids = active.map(row => Number(row.dataset.jobId)).filter(Number.isInteger); + requestInProgress = true; try { const response = await fetch('/api/software-jobs/statuses', { method: 'POST', @@ -86,10 +89,17 @@ if (activeRows().length) timer = window.setTimeout(refresh, 3000); } catch (_) { timer = window.setTimeout(refresh, 5000); + } finally { + requestInProgress = false; } } if (activeRows().length) timer = window.setTimeout(refresh, 1200); + window.addEventListener('asset-table-content-reloaded', event => { + if (event.detail?.table !== table || !activeRows().length) return; + if (timer) window.clearTimeout(timer); + timer = window.setTimeout(refresh, 250); + }); window.addEventListener('beforeunload', () => { if (timer) window.clearTimeout(timer); }, {once: true}); diff --git a/app/static/js/table-tools.js b/app/static/js/table-tools.js index 1f475a5..58799ae 100644 --- a/app/static/js/table-tools.js +++ b/app/static/js/table-tools.js @@ -142,6 +142,8 @@ const isAssetTable = table.id === 'asset-table'; const usesServerFilters = table.dataset.serverFilters === '1'; let serverFilterTimer = null; + let serverFilterController = null; + let serverFilterRequestId = 0; // Normal tables get stable synthetic keys. The asset table already has // real data-field keys on its movable columns; selection and action @@ -316,7 +318,7 @@ if (!usesServerFilters) return; if (serverFilterTimer) window.clearTimeout(serverFilterTimer); - const navigate = () => { + const navigate = async () => { const target = new URL( table.dataset.serverFilterUrl || window.location.pathname, window.location.origin @@ -331,13 +333,48 @@ if (value) target.searchParams.set(`job_filter_${definition.serverName}`, value); }); + const requestId = ++serverFilterRequestId; + serverFilterController?.abort(); + serverFilterController = new AbortController(); + table.classList.add('server-filter-loading'); + table.setAttribute('aria-busy', 'true'); + try { - window.sessionStorage.setItem( - 'assetmanager:software-jobs:last-server-filter', - document.activeElement?.name || '' + const response = await fetch(target.toString(), { + cache: 'no-store', + headers: {'X-Requested-With': 'AssetManagerTableFilter'}, + signal: serverFilterController.signal, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const html = await response.text(); + if (requestId !== serverFilterRequestId) return; + + const documentCopy = new DOMParser().parseFromString(html, 'text/html'); + const replacement = documentCopy.querySelector( + `table[data-storage-key="${CSS.escape(table.dataset.storageKey || '')}"]` ); - } catch (_) {} - window.location.assign(target.toString()); + const replacementBody = replacement?.tBodies?.[0]; + if (!replacement || !replacementBody) throw new Error('Filtered table not found'); + + tbody.replaceChildren(...[...replacementBody.rows].map(row => row.cloneNode(true))); + table.dataset.serverFilterTotal = replacement.dataset.serverFilterTotal || String(tbody.rows.length); + window.history.replaceState({}, '', target.toString()); + window.AssetManagerTime?.scan(tbody); + applyFilters(); + window.dispatchEvent(new CustomEvent('asset-table-content-reloaded', { + detail: {table} + })); + } catch (error) { + if (error?.name !== 'AbortError') { + // Fall back to a normal navigation only when the partial reload fails. + window.location.assign(target.toString()); + } + } finally { + if (requestId === serverFilterRequestId) { + table.classList.remove('server-filter-loading'); + table.removeAttribute('aria-busy'); + } + } }; if (immediate) navigate(); diff --git a/app/templates/software_summary.html b/app/templates/software_summary.html index dbe38e8..0cea2f6 100644 --- a/app/templates/software_summary.html +++ b/app/templates/software_summary.html @@ -35,8 +35,8 @@ {{ t('software.platform') }} {{ t('software.versions') }} {{ t('software.version_count') }} - {{ t('software.asset_count') }} - {{ t('software.entry_count') }} + {{ t('software.unique_asset_count') }} ⓘ + {{ t('software.inventory_entry_count') }} ⓘ {{ t('software.last_seen') }} diff --git a/app/version.py b/app/version.py index a800bd1..1941dec 100644 --- a/app/version.py +++ b/app/version.py @@ -1,2 +1,2 @@ -APP_VERSION = "0.5.5.30" +APP_VERSION = "0.5.5.31" __version__ = APP_VERSION diff --git a/docs/version-history/UPDATE-0.5.5.31.md b/docs/version-history/UPDATE-0.5.5.31.md new file mode 100644 index 0000000..983b28f --- /dev/null +++ b/docs/version-history/UPDATE-0.5.5.31.md @@ -0,0 +1,9 @@ +# Version 0.5.5.31 + +## Changes + +- Renamed the aggregated software columns to **Unique assets** and **Inventory entries**. +- Added explanatory tooltips describing the difference between distinct assets and matching inventory records. +- Reworked server-side filters in the recent jobs table to update only the table body instead of reloading the complete page. +- Kept filter controls and the active-filter indicator visible during database queries. +- Preserved live job status updates and bulk restart controls after filtered rows are refreshed.