From 637604b38401b19d2c9ef73d5729d356bb80c8e6 Mon Sep 17 00:00:00 2001 From: chen21019 Date: Wed, 7 Oct 2026 10:52:53 +0800 Subject: [PATCH] fix: preserve all-time audit scope in queries and exports --- COMPATIBILITY.md | 6 +++ README.md | 5 ++ app/admin-tab/audit-logs/controller.js | 9 ++-- app/admin-tab/audit-logs/route.js | 4 ++ ...ass-replacement.node24-ignore-scripts.json | 4 +- docs/releases/web-console-1.6.180.md | 25 +++++++++ package-lock.json | 4 +- package.json | 2 +- scripts/check-modernization-blockers | 4 +- scripts/check-ui-console-workspace | 2 +- scripts/check-ui-critical-high-dependencies | 2 +- .../admin-tab/audit-logs/controller-test.js | 52 +++++++++++++++++++ tests/unit/admin-tab/audit-logs/route-test.js | 24 ++++++++- 13 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 docs/releases/web-console-1.6.180.md diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 018c48777f..366449b556 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,5 +1,11 @@ # Compatibility Contract +Candidate `1.6.180` forwards the explicit audit `timeScope=all` contract for both +list/poll and JSON/XLSX export. It requires Server v1.6.518's matching broker +support. Explicit dates still bound the query; unset scope retains the default +24-hour window. Permission filtering, pagination, scan caps and record retention +remain backend-owned. See the [release note](docs/releases/web-console-1.6.180.md). + Web Console preserves compatible API paths, schema and resource names, action names, setting keys, authentication routes, catalog fields, orchestration framework identifiers, generated model properties, and server-provided links. Visible branding, product-owned assets, icon identifiers, package metadata, and operator documentation use PastureStack. Historical identifiers remain only where they are server data or protocol contracts and must not be mechanically replaced. diff --git a/README.md b/README.md index 5c35d01ca7..f6c9166500 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ history, authorship, licenses, and dependency notices. ## Current release +The current source targets `1.6.180`, fixing all-time audit list and export +queries with the matching Server v1.6.518 broker. See its +[release note](docs/releases/web-console-1.6.180.md). The GitHub Release, +not this source-version statement, determines publication availability. + [Web Console 1.6.179](https://github.com/PastureStack/web-console/releases/tag/1.6.179) packages the reviewed Moment 2.31.0 and compatible dependency updates already merged on `main`, plus the official shell-quote 1.11.0 security fix in both npm diff --git a/app/admin-tab/audit-logs/controller.js b/app/admin-tab/audit-logs/controller.js index 8674a6438d..e1e78f83e1 100644 --- a/app/admin-tab/audit-logs/controller.js +++ b/app/admin-tab/audit-logs/controller.js @@ -472,6 +472,7 @@ export default Controller.extend(Sortable, { clientIp : this.get('clientIp'), created_gte : this.get('createdFrom'), created_lte : this.get('createdTo'), + timeScope : this.get('timeScope'), description : this.get('description'), eventType : this.get('eventType'), format, @@ -500,12 +501,12 @@ export default Controller.extend(Sortable, { filters.authType = this.get('authType'); filters.authenticatedAsAccountId = this.get('authenticatedAsAccountId'); filters.clientIp = this.get('clientIp'); - if (this.get('timeScope') === 'all') { - filters.createdFrom = null; - filters.createdTo = null; - } else if (this.get('createdFrom') || this.get('createdTo')) { + if (this.get('createdFrom') || this.get('createdTo')) { filters.createdFrom = localDateTime(this.get('createdFrom')); filters.createdTo = localDateTime(this.get('createdTo')); + } else if (this.get('timeScope') === 'all') { + filters.createdFrom = null; + filters.createdTo = null; } filters.description = this.get('description'); filters.descriptionOperator = this.get('descriptionOperator') || 'contains'; diff --git a/app/admin-tab/audit-logs/route.js b/app/admin-tab/audit-logs/route.js index b30d266042..987257a82c 100644 --- a/app/admin-tab/audit-logs/route.js +++ b/app/admin-tab/audit-logs/route.js @@ -191,6 +191,10 @@ export default Route.extend({ if (params.createdTo) { returnValue.filter.created_lte = params.createdTo; } + if (params.timeScope) { + // The broker otherwise applies its default 24-hour window. + returnValue.filter.timeScope = params.timeScope; + } ['accountId', 'authenticatedAsAccountId', 'resourceType', 'resourceId', 'clientIp', 'authType', 'interactionChannel'].forEach((key) => { if (params[key]) { diff --git a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json index fcbc54a8f2..640ee98437 100644 --- a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json +++ b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.179", + "version": "1.6.180", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.179", + "version": "1.6.180", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/docs/releases/web-console-1.6.180.md b/docs/releases/web-console-1.6.180.md new file mode 100644 index 0000000000..f36365af4a --- /dev/null +++ b/docs/releases/web-console-1.6.180.md @@ -0,0 +1,25 @@ +# Web Console 1.6.180 — audit all-time query contract + +The audit list, automatic refresh and JSON/XLSX exports now forward +`timeScope=all` to the permission-bound console broker. Previously the URL and +visible controls said all time, but the missing API parameter caused the broker +to apply its normal 24-hour default. Retained older records therefore appeared +absent; this was not database deletion. + +Explicit date bounds remain effective even if a bookmarked URL also contains +the all-time marker, and the editable date controls display those same bounds. +Unset scope retains the existing default. The backend remains authoritative for +scope/date validation, authorization, pagination and the 20,000-row scan limit. +All time means all records still retained, not recovery of expired records. +This component requires the matching broker contract in Server v1.6.518. + +Focused unit regressions cover scope forwarding, default behavior, explicit +dates, invalid-scope forwarding, JSON/XLSX export and draft/query agreement. +Publication uses the existing exact-source validation workflow, Chrome tests +and two byte-identical production archives. The release records the actual CI +and archive checksum after they succeed; this note is not test evidence. + +Dependencies, authentication/session protections, permissions, HAProxy and +retention settings are unchanged. Component CI, isolated deployed browser +acceptance and company production deployment are separate results. Historical +HOLDs and the incomplete full permission matrix are not promoted by this fix. diff --git a/package-lock.json b/package-lock.json index fcbc54a8f2..640ee98437 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.179", + "version": "1.6.180", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.179", + "version": "1.6.180", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package.json b/package.json index 00a9c70bd2..bb2844a4c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.179", + "version": "1.6.180", "private": true, "description": "PastureStack browser console for the compatible control platform.", "repository": { diff --git a/scripts/check-modernization-blockers b/scripts/check-modernization-blockers index 660da49668..59875db8da 100755 --- a/scripts/check-modernization-blockers +++ b/scripts/check-modernization-blockers @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f: print(json.load(f).get('version', '')) PY ) -if [[ "$version" != "1.6.179" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.179" +if [[ "$version" != "1.6.180" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.180" failures=$((failures + 1)) fi diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index 0388823f48..63a87ddfcf 100755 --- a/scripts/check-ui-console-workspace +++ b/scripts/check-ui-console-workspace @@ -143,4 +143,4 @@ if [[ -n ${PASTURESTACK_PRIVATE_MARKER:-} ]] && grep -RInF -- "$PASTURESTACK_PRI fi printf 'UI_CONSOLE_WORKSPACE_OK version=%s persistence=%s cross_tab=%s\n' \ - 1.6.179 browser-session broker-broadcast + 1.6.180 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index 79bee5949b..8812a34a4d 100755 --- a/scripts/check-ui-critical-high-dependencies +++ b/scripts/check-ui-critical-high-dependencies @@ -70,7 +70,7 @@ if lock_bytes != baseline_bytes: lock = json.loads(lock_bytes) packages = lock.get("packages", {}) root = packages.get("", {}) -if package.get("version") != "1.6.179": +if package.get("version") != "1.6.180": fail(f"unexpected Web Console version: {package.get('version')}") if root.get("version") != package.get("version"): fail(f"lock root version differs: {root.get('version')}") diff --git a/tests/unit/admin-tab/audit-logs/controller-test.js b/tests/unit/admin-tab/audit-logs/controller-test.js index ae8f5f2e01..4ba63df689 100644 --- a/tests/unit/admin-tab/audit-logs/controller-test.js +++ b/tests/unit/admin-tab/audit-logs/controller-test.js @@ -38,6 +38,58 @@ function controllerFor(properties = {}) { module('Unit | Controller | admin tab | audit logs'); +test('all-time JSON and XLSX exports use the same scope and authorization filters as the list', function(assert) { + let controller = controllerFor({ + timeScope: 'all', accountId: '1e1', authenticatedAsAccountId: '1a1', + createdFrom: null, createdTo: null, eventType: 'resource.', eventTypeOperator: 'startsWith', + }); + + ['json', 'xlsx'].forEach((format) => { + // download() reuses this element: capture the URL without initiating a network request. + let target = document.createElement('div'); + target.id = `audit-log-export-${format}`; + document.body.appendChild(target); + try { + controller.actions.exportLogs.call(controller, format); + let query = new URL(target.src, window.location.origin); + assert.strictEqual(query.pathname, '/v2-beta/pasturestack/audit-logs/export'); + assert.strictEqual(query.searchParams.get('timeScope'), 'all', `${format} preserves all-time intent`); + assert.strictEqual(query.searchParams.get('format'), format); + assert.strictEqual(query.searchParams.get('accountId'), '1e1'); + assert.strictEqual(query.searchParams.get('authenticatedAsAccountId'), '1a1'); + assert.strictEqual(query.searchParams.get('eventType_prefix'), 'resource.'); + assert.notOk(query.searchParams.has('created_gte'), 'no artificial start date'); + assert.notOk(query.searchParams.has('created_lte'), 'no artificial end date'); + + controller.setProperties({createdFrom: '2026-09-01T00:00:00.000Z', createdTo: '2026-10-01T00:00:00.000Z'}); + controller.actions.exportLogs.call(controller, format); + query = new URL(target.src, window.location.origin); + assert.strictEqual(query.searchParams.get('created_gte'), controller.get('createdFrom')); + assert.strictEqual(query.searchParams.get('created_lte'), controller.get('createdTo')); + controller.setProperties({timeScope: null, createdFrom: null, createdTo: null}); + controller.actions.exportLogs.call(controller, format); + query = new URL(target.src, window.location.origin); + assert.notOk(query.searchParams.has('timeScope'), 'the normal default is not promoted to all time'); + controller.set('timeScope', 'all'); + } finally { + target.remove(); + } + }); + + destroyOwned(controller); +}); + +test('a bookmarked all-time marker does not hide explicit date bounds in the draft', function(assert) { + let controller = controllerFor({ + timeScope: 'all', createdFrom: '2026-09-01T00:00:00.000Z', createdTo: '2026-10-01T00:00:00.000Z', + }); + controller.syncDraftFromQuery(); + assert.strictEqual(moment(controller.get('filters.createdFrom')).toISOString(), controller.get('createdFrom')); + assert.strictEqual(moment(controller.get('filters.createdTo')).toISOString(), controller.get('createdTo')); + assert.notEqual(controller.get('activeTimePreset'), 'all', 'the displayed range agrees with the API query'); + destroyOwned(controller); +}); + test('offers friendly environment and user names without raw ID fallbacks', function(assert) { let controller = controllerFor({ model: EmberObject.create({ diff --git a/tests/unit/admin-tab/audit-logs/route-test.js b/tests/unit/admin-tab/audit-logs/route-test.js index 0ef63ebcb8..dc56d5e616 100644 --- a/tests/unit/admin-tab/audit-logs/route-test.js +++ b/tests/unit/admin-tab/audit-logs/route-test.js @@ -65,8 +65,28 @@ test('supports useful text operators without leaking UI-only fields', function(a 'the presentation-only operator never reaches GDAPI'); assert.strictEqual(route.parseFilters({interactionChannel: 'web_ui'}).filter.interactionChannel, 'web_ui', 'interaction channel is forwarded to the permission-bound endpoint'); - assert.notOk('timeScope' in route.parseFilters({timeScope: 'all'}).filter, - 'the frontend all-time marker never reaches GDAPI'); + + destroyOwned(route); +}); + +test('forwards all-time intent while preserving dates, permissions and polling bounds', function(assert) { + let route = createOwned(AuditLogsRoute, {}, 'route'); + let query = route.parseFilters({timeScope: 'all', accountId: '1a5'}); + + assert.deepEqual(query.filter, {timeScope: 'all', accountId: '1a5'}, + 'an all-time request reaches the permission-bound broker without invented dates'); + assert.strictEqual(query.url, 'pasturestack/audit-logs'); + assert.strictEqual(query.limit, 100); + assert.strictEqual(query.depaginate, false); + assert.deepEqual(route.parseFilters({}).filter, {}, 'the default still uses the broker 24-hour window'); + assert.deepEqual(route.parseFilters({timeScope: null}).filter, {}, 'an unset scope is omitted'); + assert.deepEqual(route.parseFilters({ + timeScope: 'all', createdFrom: '2026-09-01T00:00:00.000Z', createdTo: '2026-10-01T00:00:00.000Z', + }).filter, { + timeScope: 'all', created_gte: '2026-09-01T00:00:00.000Z', created_lte: '2026-10-01T00:00:00.000Z', + }, 'explicit dates are never dropped when a bookmarked URL also contains all'); + assert.strictEqual(route.parseFilters({timeScope: 'invalid'}).filter.timeScope, 'invalid', + 'the authoritative broker can reject an invalid bookmarked scope instead of silently changing it'); destroyOwned(route); });