From c61dc7cd4dd8c874b5cd7bb1dbe46ac2ac4e126d Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Thu, 10 Sep 2026 14:26:59 +0100 Subject: [PATCH 1/7] Fix NinjaOne queries returning no data, and make ts a working timeframe filter Nine NinjaOne data streams showed "No data" in SquaredUp while the same data was visible in the NinjaOne console and retrievable via the same API. All of them sent `ts={{timeframe.end}}` to `/v2/queries/*`. NinjaOne's `ts` ("Monitoring timestamp filter", documented only as `type: string`) is a filter *expression*, not a bare timestamp - an unparseable value returns HTTP 500 `InvalidFilterException`. A bare value is an **exact-match** test against the record's collection timestamp, so any single timestamp - ISO or epoch, seconds or millis - essentially never matches and returns zero rows. Verified against a live tenant: ts= -> 0 rows (what shipped) ts= -> 0 rows ts= -> 0 rows ts= -> 1 row (exact match) ts=after -> filters correctly (omitted) -> full data So re-encoding the value as epoch would not have helped; the expression form is what `ts` wants. The streams now send `after `, which boundary-tests confirmed filters exactly on each row's collection timestamp. The 12 affected streams split three ways, because only 8 endpoints honour `ts`: - 8 streams gain a real timeframe filter. `last1hour`/`last12hours` are dropped from their `timeframes` - NinjaOne re-scans inventory daily-to-weekly, so those windows cannot return rows on any tenant, which is what produced the original report. `defaultTimeframe: "none"` keeps new tiles on current state. - `volumesGlobal` drops `ts` entirely: its filter targets an enrollment-era value while its `timestamp` column is regenerated per request, so a picker there would filter on something other than the column displayed. - `networkInterfacesGlobal`, `policyOverrides` and `windowsServices` drop `ts` as dead config - those endpoints do not define it and NinjaOne discards it, which is why they appeared to work. The `ts` value is guarded so that "None", an absent timeframe, or a missing `unixStart` omit the argument rather than send an empty one, which would 500. That matters beyond the error: any `ts` value silently drops records that have no timestamp at all - on `antivirusStatus` that is the device reporting no antivirus product, exactly the row a security dashboard must not hide. Co-Authored-By: Claude Opus 5 --- .../v1/dataStreams/antivirusStatus.json | 8 +++---- .../v1/dataStreams/antivirusThreats.json | 8 +++---- .../v1/dataStreams/computerSystems.json | 8 +++---- .../NinjaOne/v1/dataStreams/disksGlobal.json | 8 +++---- .../dataStreams/networkInterfacesGlobal.json | 22 ++----------------- .../v1/dataStreams/operatingSystems.json | 8 +++---- .../NinjaOne/v1/dataStreams/osPatches.json | 8 +++---- .../v1/dataStreams/policyOverrides.json | 22 ++----------------- .../v1/dataStreams/processorsGlobal.json | 8 +++---- .../v1/dataStreams/softwarePatches.json | 8 +++---- .../v1/dataStreams/volumesGlobal.json | 19 +--------------- .../v1/dataStreams/windowsServices.json | 22 ++----------------- plugins/NinjaOne/v1/metadata.json | 2 +- 13 files changed, 40 insertions(+), 111 deletions(-) diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json index e12c25ee..012c7662 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json @@ -28,7 +28,7 @@ "getArgs": [ { "key": "ts", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' || timeframe.unixStart == null ? null : 'after ' + timeframe.unixStart}}" } ], "headers": [] @@ -87,8 +87,6 @@ } ], "timeframes": [ - "last1hour", - "last12hours", "last24hours", "last7days", "last30days", @@ -99,5 +97,7 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" } diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json index 24120443..3cce7cae 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json @@ -28,7 +28,7 @@ "getArgs": [ { "key": "ts", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' || timeframe.unixStart == null ? null : 'after ' + timeframe.unixStart}}" } ], "headers": [] @@ -103,8 +103,6 @@ } ], "timeframes": [ - "last1hour", - "last12hours", "last24hours", "last7days", "last30days", @@ -115,5 +113,7 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" } diff --git a/plugins/NinjaOne/v1/dataStreams/computerSystems.json b/plugins/NinjaOne/v1/dataStreams/computerSystems.json index f2e63712..12c463fe 100644 --- a/plugins/NinjaOne/v1/dataStreams/computerSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/computerSystems.json @@ -28,7 +28,7 @@ "getArgs": [ { "key": "ts", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' || timeframe.unixStart == null ? null : 'after ' + timeframe.unixStart}}" } ], "headers": [] @@ -82,8 +82,6 @@ } ], "timeframes": [ - "last1hour", - "last12hours", "last24hours", "last7days", "last30days", @@ -94,5 +92,7 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" } diff --git a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json index 58dc1ce3..5f19cad6 100644 --- a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json @@ -28,7 +28,7 @@ "getArgs": [ { "key": "ts", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' || timeframe.unixStart == null ? null : 'after ' + timeframe.unixStart}}" } ], "headers": [] @@ -107,8 +107,6 @@ } ], "timeframes": [ - "last1hour", - "last12hours", "last24hours", "last7days", "last30days", @@ -119,5 +117,7 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" } diff --git a/plugins/NinjaOne/v1/dataStreams/networkInterfacesGlobal.json b/plugins/NinjaOne/v1/dataStreams/networkInterfacesGlobal.json index 22be1bee..3c7ee60d 100644 --- a/plugins/NinjaOne/v1/dataStreams/networkInterfacesGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/networkInterfacesGlobal.json @@ -25,12 +25,7 @@ "expandInnerObjects": true, "endpointPath": "/v2/queries/network-interfaces", "pathToData": "results", - "getArgs": [ - { - "key": "ts", - "value": "{{timeframe.end}}" - } - ], + "getArgs": [], "headers": [] }, "metadata": [ @@ -116,18 +111,5 @@ "pattern": ".*" } ], - "timeframes": [ - "last1hour", - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth", - "thisQuarter", - "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", - "none" - ] + "timeframes": false } diff --git a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json index 887a16f8..3f921486 100644 --- a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json @@ -28,7 +28,7 @@ "getArgs": [ { "key": "ts", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' || timeframe.unixStart == null ? null : 'after ' + timeframe.unixStart}}" } ], "headers": [] @@ -97,8 +97,6 @@ } ], "timeframes": [ - "last1hour", - "last12hours", "last24hours", "last7days", "last30days", @@ -109,5 +107,7 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" } diff --git a/plugins/NinjaOne/v1/dataStreams/osPatches.json b/plugins/NinjaOne/v1/dataStreams/osPatches.json index aa7b80b7..15626a62 100644 --- a/plugins/NinjaOne/v1/dataStreams/osPatches.json +++ b/plugins/NinjaOne/v1/dataStreams/osPatches.json @@ -28,7 +28,7 @@ "getArgs": [ { "key": "ts", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' || timeframe.unixStart == null ? null : 'after ' + timeframe.unixStart}}" } ], "headers": [] @@ -98,8 +98,6 @@ } ], "timeframes": [ - "last1hour", - "last12hours", "last24hours", "last7days", "last30days", @@ -110,5 +108,7 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" } diff --git a/plugins/NinjaOne/v1/dataStreams/policyOverrides.json b/plugins/NinjaOne/v1/dataStreams/policyOverrides.json index 771293f7..d6be48f2 100644 --- a/plugins/NinjaOne/v1/dataStreams/policyOverrides.json +++ b/plugins/NinjaOne/v1/dataStreams/policyOverrides.json @@ -25,12 +25,7 @@ "expandInnerObjects": true, "endpointPath": "/v2/queries/policy-overrides", "pathToData": "results", - "getArgs": [ - { - "key": "ts", - "value": "{{timeframe.end}}" - } - ], + "getArgs": [], "headers": [] }, "metadata": [ @@ -91,18 +86,5 @@ "pattern": ".*" } ], - "timeframes": [ - "last1hour", - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth", - "thisQuarter", - "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", - "none" - ] + "timeframes": false } diff --git a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json index 3a0dcef7..fcaebc79 100644 --- a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json @@ -28,7 +28,7 @@ "getArgs": [ { "key": "ts", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' || timeframe.unixStart == null ? null : 'after ' + timeframe.unixStart}}" } ], "headers": [] @@ -102,8 +102,6 @@ } ], "timeframes": [ - "last1hour", - "last12hours", "last24hours", "last7days", "last30days", @@ -114,5 +112,7 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" } diff --git a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json index aefa1408..b4ea50d4 100644 --- a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json +++ b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json @@ -28,7 +28,7 @@ "getArgs": [ { "key": "ts", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' || timeframe.unixStart == null ? null : 'after ' + timeframe.unixStart}}" } ], "headers": [] @@ -103,8 +103,6 @@ } ], "timeframes": [ - "last1hour", - "last12hours", "last24hours", "last7days", "last30days", @@ -115,5 +113,7 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" } diff --git a/plugins/NinjaOne/v1/dataStreams/volumesGlobal.json b/plugins/NinjaOne/v1/dataStreams/volumesGlobal.json index 6ad3113a..12c74d33 100644 --- a/plugins/NinjaOne/v1/dataStreams/volumesGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/volumesGlobal.json @@ -29,10 +29,6 @@ { "key": "include", "value": "bl" - }, - { - "key": "ts", - "value": "{{timeframe.end}}" } ], "headers": [] @@ -94,18 +90,5 @@ "pattern": ".*" } ], - "timeframes": [ - "last1hour", - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth", - "thisQuarter", - "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", - "none" - ] + "timeframes": false } diff --git a/plugins/NinjaOne/v1/dataStreams/windowsServices.json b/plugins/NinjaOne/v1/dataStreams/windowsServices.json index a356b477..d544ca52 100644 --- a/plugins/NinjaOne/v1/dataStreams/windowsServices.json +++ b/plugins/NinjaOne/v1/dataStreams/windowsServices.json @@ -25,12 +25,7 @@ "expandInnerObjects": false, "endpointPath": "/v2/queries/windows-services", "pathToData": "results", - "getArgs": [ - { - "key": "ts", - "value": "{{timeframe.end}}" - } - ], + "getArgs": [], "headers": [] }, "metadata": [ @@ -86,18 +81,5 @@ "pattern": ".*" } ], - "timeframes": [ - "last1hour", - "last12hours", - "last24hours", - "last7days", - "last30days", - "thisMonth", - "thisQuarter", - "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", - "none" - ] + "timeframes": false } diff --git a/plugins/NinjaOne/v1/metadata.json b/plugins/NinjaOne/v1/metadata.json index 47e179a8..69b8399b 100644 --- a/plugins/NinjaOne/v1/metadata.json +++ b/plugins/NinjaOne/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "ninja-one", "displayName": "NinjaOne", - "version": "1.1.14", + "version": "1.2.0", "author": { "name": "SquaredUp Labs", "type": "labs" From 31e67b083f9d29c584e9fa67a2a4b868bbb8e03c Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Thu, 10 Sep 2026 17:32:53 +0100 Subject: [PATCH 2/7] Withdraw closed historical timeframes that ts cannot bound Review feedback on #131 asked whether the filter should also bound to `timeframe.unixEnd`. It should, and it can't: `ts` accepts exactly one clause. Tested against the live API on /v2/queries/antivirus-status: after 200, filters correctly before 200, filters correctly (alone) after X and before Y 500 InvalidFilterException after X,before Y / X before Y 500 InvalidFilterException >X and --- plugins/NinjaOne/v1/dataStreams/antivirusStatus.json | 3 --- plugins/NinjaOne/v1/dataStreams/antivirusThreats.json | 3 --- plugins/NinjaOne/v1/dataStreams/computerSystems.json | 3 --- plugins/NinjaOne/v1/dataStreams/disksGlobal.json | 3 --- plugins/NinjaOne/v1/dataStreams/operatingSystems.json | 3 --- plugins/NinjaOne/v1/dataStreams/osPatches.json | 3 --- plugins/NinjaOne/v1/dataStreams/processorsGlobal.json | 3 --- plugins/NinjaOne/v1/dataStreams/softwarePatches.json | 3 --- 8 files changed, 24 deletions(-) diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json index 012c7662..537dc901 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json @@ -93,9 +93,6 @@ "thisMonth", "thisQuarter", "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json index 3cce7cae..97979da7 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json @@ -109,9 +109,6 @@ "thisMonth", "thisQuarter", "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/computerSystems.json b/plugins/NinjaOne/v1/dataStreams/computerSystems.json index 12c463fe..b5b66e50 100644 --- a/plugins/NinjaOne/v1/dataStreams/computerSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/computerSystems.json @@ -88,9 +88,6 @@ "thisMonth", "thisQuarter", "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json index 5f19cad6..2c83ea9c 100644 --- a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json @@ -113,9 +113,6 @@ "thisMonth", "thisQuarter", "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json index 3f921486..16f6e01c 100644 --- a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json @@ -103,9 +103,6 @@ "thisMonth", "thisQuarter", "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/osPatches.json b/plugins/NinjaOne/v1/dataStreams/osPatches.json index 15626a62..c393b023 100644 --- a/plugins/NinjaOne/v1/dataStreams/osPatches.json +++ b/plugins/NinjaOne/v1/dataStreams/osPatches.json @@ -104,9 +104,6 @@ "thisMonth", "thisQuarter", "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json index fcaebc79..f3fbfb58 100644 --- a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json @@ -108,9 +108,6 @@ "thisMonth", "thisQuarter", "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json index b4ea50d4..7675e60c 100644 --- a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json +++ b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json @@ -109,9 +109,6 @@ "thisMonth", "thisQuarter", "thisYear", - "lastMonth", - "lastQuarter", - "lastYear", "none" ], "supportsNoneTimeframe": true, From 4238fc55b9651004823c736be62f5f9c47672db0 Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Fri, 11 Sep 2026 10:00:09 +0100 Subject: [PATCH 3/7] Offer every timeframe the NinjaOne endpoints can actually support Audited all 37 streams' endpoints against the NinjaOne spec for real date filters, then compared that against what each stream exposes. Two false leads discarded first: `after` on /v2/devices, /v2/organizations and /v2/locations is a paging cursor ("Last Node ID from previous page"), not a date filter, and `tz` on /v2/alerts and /v2/jobs is a Time Zone. Three genuine gaps remained. 1. The 8 `ts` streams get all 12 windows back. `last1hour`/`last12hours` were never broken - they were withdrawn only because they are usually empty (nothing is re-scanned that often). The three closed windows needed an upper bound, which `ts` cannot express, so the new shared `collectionWindow.js` applies it after the response while the request keeps `after unixStart`. Same split as Vercel's deployments.js. It costs nothing: these endpoints are snapshots, not history - `after 1` returns the same row count as an unfiltered request - so the filter never sees more than one inventory table. `pathToData` is dropped from those 8, since it is ignored once a script is set. 2. backupJobs' three closed windows were silently wrong. It offered all 12 but sent `startTime after {{timeframe.start}}` - lower bound only - so they over-returned. Unlike `ts`, `stf` supports `between A and B`, so this needed no script. 3. software (scoped) can support timeframes and didn't. /v2/queries/software offers installedAfter *and* installedBefore, filtering on genuine install date, and its own twin softwareGlobal already used both. The scoped variant being `timeframes: false` was an accident, not a decision. Along the way: `timeframe.start`/`end` still resolve to a default 24-hour window when a tile is set to "None", so every stream interpolating them needs an explicit `enum === 'none'` check or it silently applies a 24-hour filter to a request the user asked to be unfiltered. softwareGlobal had exactly that bug and returned 0 rows at "None"; it now returns 407. backupJobs was the same. Both are guarded, and both now declare `supportsNoneTimeframe`, which "none" in a timeframes array requires. The `timestamp` column is now declared on the 7 streams that relied on the `.*` catch-all, which was rendering it via shape_number as "1,788,918,329.37" instead of a date. Descriptions gained a bracketed note on what the timeframe filters on, since these endpoints hold only current state - "lastMonth" can mean "devices whose most recent scan fell in August", never "what the estate looked like in August". Co-Authored-By: Claude Opus 5 --- .../v1/dataStreams/antivirusStatus.json | 9 ++++-- .../v1/dataStreams/antivirusThreats.json | 19 ++++++++++-- .../NinjaOne/v1/dataStreams/backupJobs.json | 5 ++-- .../v1/dataStreams/computerSystems.json | 19 ++++++++++-- .../NinjaOne/v1/dataStreams/disksGlobal.json | 19 ++++++++++-- .../v1/dataStreams/operatingSystems.json | 19 ++++++++++-- .../NinjaOne/v1/dataStreams/osPatches.json | 19 ++++++++++-- .../v1/dataStreams/processorsGlobal.json | 19 ++++++++++-- .../dataStreams/scripts/collectionWindow.js | 25 ++++++++++++++++ plugins/NinjaOne/v1/dataStreams/software.json | 29 +++++++++++++++++-- .../v1/dataStreams/softwareGlobal.json | 7 +++-- .../v1/dataStreams/softwarePatches.json | 19 ++++++++++-- 12 files changed, 184 insertions(+), 24 deletions(-) create mode 100644 plugins/NinjaOne/v1/dataStreams/scripts/collectionWindow.js diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json index 537dc901..f4251fb7 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json @@ -2,7 +2,7 @@ "name": "antivirusStatus", "displayName": "Antivirus Status", "tags": ["Security"], - "description": "Antivirus product status and definition updates", + "description": "Antivirus product status and definition updates (timeframe filters on when each record was last collected)", "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -24,7 +24,7 @@ }, "expandInnerObjects": false, "endpointPath": "/v2/queries/antivirus-status", - "pathToData": "results", + "postRequestScript": "collectionWindow.js", "getArgs": [ { "key": "ts", @@ -87,12 +87,17 @@ } ], "timeframes": [ + "last1hour", + "last12hours", "last24hours", "last7days", "last30days", "thisMonth", "thisQuarter", "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json index 97979da7..e159c103 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json @@ -2,7 +2,7 @@ "name": "antivirusThreats", "displayName": "Antivirus Threats", "tags": ["Security"], - "description": "Detected antivirus threats with quarantine status", + "description": "Detected antivirus threats with quarantine status (timeframe filters on when each record was last collected)", "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -24,7 +24,7 @@ }, "expandInnerObjects": false, "endpointPath": "/v2/queries/antivirus-threats", - "pathToData": "results", + "postRequestScript": "collectionWindow.js", "getArgs": [ { "key": "ts", @@ -98,17 +98,32 @@ "displayName": "Action Taken", "shape": "string" }, + { + "name": "timestamp", + "displayName": "Last Updated", + "shape": [ + "date", + { + "timeZone": "Etc/UTC" + } + ] + }, { "pattern": ".*" } ], "timeframes": [ + "last1hour", + "last12hours", "last24hours", "last7days", "last30days", "thisMonth", "thisQuarter", "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/backupJobs.json b/plugins/NinjaOne/v1/dataStreams/backupJobs.json index c7776f98..2ddddf6e 100644 --- a/plugins/NinjaOne/v1/dataStreams/backupJobs.json +++ b/plugins/NinjaOne/v1/dataStreams/backupJobs.json @@ -28,7 +28,7 @@ "getArgs": [ { "key": "stf", - "value": "startTime after {{timeframe.start}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' || timeframe.start == null || timeframe.end == null ? null : 'startTime between ' + timeframe.start + ' and ' + timeframe.end}}" } ], "headers": [] @@ -178,5 +178,6 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true } diff --git a/plugins/NinjaOne/v1/dataStreams/computerSystems.json b/plugins/NinjaOne/v1/dataStreams/computerSystems.json index b5b66e50..52c2bf1e 100644 --- a/plugins/NinjaOne/v1/dataStreams/computerSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/computerSystems.json @@ -2,7 +2,7 @@ "name": "computerSystems", "displayName": "Computer Systems", "tags": ["Monitoring"], - "description": "Hardware information including manufacturer, model, and serial number", + "description": "Hardware information including manufacturer, model, and serial number (timeframe filters on when each record was last collected)", "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -24,7 +24,7 @@ }, "expandInnerObjects": false, "endpointPath": "/v2/queries/computer-systems", - "pathToData": "results", + "postRequestScript": "collectionWindow.js", "getArgs": [ { "key": "ts", @@ -77,17 +77,32 @@ "displayName": "Workgroup", "shape": "string" }, + { + "name": "timestamp", + "displayName": "Last Updated", + "shape": [ + "date", + { + "timeZone": "Etc/UTC" + } + ] + }, { "pattern": ".*" } ], "timeframes": [ + "last1hour", + "last12hours", "last24hours", "last7days", "last30days", "thisMonth", "thisQuarter", "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json index 2c83ea9c..8e3bfcd0 100644 --- a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json @@ -2,7 +2,7 @@ "name": "disksGlobal", "displayName": "Devices (Disks Global)", "tags": ["Monitoring"], - "description": "Physical disk inventory with SMART status across all devices", + "description": "Physical disk inventory with SMART status across all devices (timeframe filters on when each record was last collected)", "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -24,7 +24,7 @@ }, "expandInnerObjects": false, "endpointPath": "/v2/queries/disks", - "pathToData": "results", + "postRequestScript": "collectionWindow.js", "getArgs": [ { "key": "ts", @@ -102,17 +102,32 @@ } ] }, + { + "name": "timestamp", + "displayName": "Last Updated", + "shape": [ + "date", + { + "timeZone": "Etc/UTC" + } + ] + }, { "pattern": ".*" } ], "timeframes": [ + "last1hour", + "last12hours", "last24hours", "last7days", "last30days", "thisMonth", "thisQuarter", "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json index 16f6e01c..bfe22dab 100644 --- a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json @@ -2,7 +2,7 @@ "name": "operatingSystems", "displayName": "Devices (Operating Systems)", "tags": ["Monitoring"], - "description": "OS details with reboot requirements", + "description": "OS details with reboot requirements (timeframe filters on when each record was last collected)", "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -24,7 +24,7 @@ }, "expandInnerObjects": false, "endpointPath": "/v2/queries/operating-systems", - "pathToData": "results", + "postRequestScript": "collectionWindow.js", "getArgs": [ { "key": "ts", @@ -92,17 +92,32 @@ } ] }, + { + "name": "timestamp", + "displayName": "Last Updated", + "shape": [ + "date", + { + "timeZone": "Etc/UTC" + } + ] + }, { "pattern": ".*" } ], "timeframes": [ + "last1hour", + "last12hours", "last24hours", "last7days", "last30days", "thisMonth", "thisQuarter", "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/osPatches.json b/plugins/NinjaOne/v1/dataStreams/osPatches.json index c393b023..1321e8cb 100644 --- a/plugins/NinjaOne/v1/dataStreams/osPatches.json +++ b/plugins/NinjaOne/v1/dataStreams/osPatches.json @@ -2,7 +2,7 @@ "name": "osPatches", "displayName": "OS Patches", "tags": ["Security"], - "description": "OS patch compliance status including pending, failed, and installed patches", + "description": "OS patch compliance status including pending, failed, and installed patches (timeframe filters on when each record was last collected)", "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -24,7 +24,7 @@ }, "expandInnerObjects": false, "endpointPath": "/v2/queries/os-patches", - "pathToData": "results", + "postRequestScript": "collectionWindow.js", "getArgs": [ { "key": "ts", @@ -93,17 +93,32 @@ } ] }, + { + "name": "timestamp", + "displayName": "Last Updated", + "shape": [ + "date", + { + "timeZone": "Etc/UTC" + } + ] + }, { "pattern": ".*" } ], "timeframes": [ + "last1hour", + "last12hours", "last24hours", "last7days", "last30days", "thisMonth", "thisQuarter", "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json index f3fbfb58..1b3edfc7 100644 --- a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json @@ -2,7 +2,7 @@ "name": "processorsGlobal", "displayName": "Devices (Processors Global)", "tags": ["Monitoring"], - "description": "CPU information and specifications across all devices", + "description": "CPU information and specifications across all devices (timeframe filters on when each record was last collected)", "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -24,7 +24,7 @@ }, "expandInnerObjects": false, "endpointPath": "/v2/queries/processors", - "pathToData": "results", + "postRequestScript": "collectionWindow.js", "getArgs": [ { "key": "ts", @@ -97,17 +97,32 @@ } ] }, + { + "name": "timestamp", + "displayName": "Last Updated", + "shape": [ + "date", + { + "timeZone": "Etc/UTC" + } + ] + }, { "pattern": ".*" } ], "timeframes": [ + "last1hour", + "last12hours", "last24hours", "last7days", "last30days", "thisMonth", "thisQuarter", "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", "none" ], "supportsNoneTimeframe": true, diff --git a/plugins/NinjaOne/v1/dataStreams/scripts/collectionWindow.js b/plugins/NinjaOne/v1/dataStreams/scripts/collectionWindow.js new file mode 100644 index 00000000..e4d07624 --- /dev/null +++ b/plugins/NinjaOne/v1/dataStreams/scripts/collectionWindow.js @@ -0,0 +1,25 @@ +// Applies the upper end of the timeframe to a /v2/queries/* monitoring snapshot. +// +// NinjaOne's `ts` filter accepts exactly one clause - `after X` and `before Y` each work +// alone, but `after X and before Y`, `between X and Y` and a repeated `ts` arg all fail +// (500 InvalidFilterException, or the second clause is silently ignored). So the request +// supplies the lower bound as `after unixStart` and the upper bound is applied here. Same +// split as Vercel's deployments.js, for the same reason. +// +// Cheap by construction: these endpoints return one current row per item and hold no +// history, so a wider window never returns more rows than an unfiltered request - this +// trims at most one inventory table. +const items = (data && data.results ? data.results : (Array.isArray(data) ? data : [])); + +// `timeframe.start`/`end` still resolve to a default 24-hour window when the tile is set to +// "None", so the enum has to be checked explicitly - reading unixEnd alone would apply a +// silent 24-hour filter to a request the user asked to be unfiltered. +const tf = context.timeframe || {}; +const endTime = tf.enum === 'none' ? null : tf.unixEnd; + +// Rows with no `timestamp` never reach this filter: whenever a window is selected the +// request carries `after unixStart`, which NinjaOne already applies server-side, and it +// drops them. The typeof check is belt-and-braces. +result = typeof endTime === 'number' + ? items.filter((item) => typeof item.timestamp === 'number' && item.timestamp <= endTime) + : items; diff --git a/plugins/NinjaOne/v1/dataStreams/software.json b/plugins/NinjaOne/v1/dataStreams/software.json index e9ecdf10..c5524835 100644 --- a/plugins/NinjaOne/v1/dataStreams/software.json +++ b/plugins/NinjaOne/v1/dataStreams/software.json @@ -2,7 +2,7 @@ "name": "software", "displayName": "Software", "tags": ["Security"], - "description": "Installed software inventory", + "description": "Installed software inventory (timeframe filters on install date)", "baseDataSourceName": "httpRequestScoped", "config": { "httpMethod": "get", @@ -26,7 +26,16 @@ "endpointPath": "/v2/queries/software", "postRequestScript": "software.js", "pathToData": "results", - "getArgs": [], + "getArgs": [ + { + "key": "installedAfter", + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' ? null : timeframe.start}}" + }, + { + "key": "installedBefore", + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' ? null : timeframe.end}}" + } + ], "headers": [] }, "matches": { @@ -81,5 +90,19 @@ "pattern": ".*" } ], - "timeframes": false + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth", + "thisQuarter", + "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", + "none" + ], + "supportsNoneTimeframe": true } diff --git a/plugins/NinjaOne/v1/dataStreams/softwareGlobal.json b/plugins/NinjaOne/v1/dataStreams/softwareGlobal.json index 63414aae..37df0e77 100644 --- a/plugins/NinjaOne/v1/dataStreams/softwareGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/softwareGlobal.json @@ -28,11 +28,11 @@ "getArgs": [ { "key": "installedAfter", - "value": "{{timeframe.start}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' ? null : timeframe.start}}" }, { "key": "installedBefore", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' ? null : timeframe.end}}" } ], "headers": [] @@ -102,5 +102,6 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true } diff --git a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json index 7675e60c..8b4ea8af 100644 --- a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json +++ b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json @@ -2,7 +2,7 @@ "name": "softwarePatches", "displayName": "Software Patches", "tags": ["Security"], - "description": "Third-party software patch compliance status including pending, failed, and installed patches", + "description": "Third-party software patch compliance status including pending, failed, and installed patches (timeframe filters on when each record was last collected)", "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", @@ -24,7 +24,7 @@ }, "expandInnerObjects": false, "endpointPath": "/v2/queries/software-patches", - "pathToData": "results", + "postRequestScript": "collectionWindow.js", "getArgs": [ { "key": "ts", @@ -98,17 +98,32 @@ } ] }, + { + "name": "timestamp", + "displayName": "Last Updated", + "shape": [ + "date", + { + "timeZone": "Etc/UTC" + } + ] + }, { "pattern": ".*" } ], "timeframes": [ + "last1hour", + "last12hours", "last24hours", "last7days", "last30days", "thisMonth", "thisQuarter", "thisYear", + "lastMonth", + "lastQuarter", + "lastYear", "none" ], "supportsNoneTimeframe": true, From 0cd9b3fe241a4a82be0587a4afb9054b205983d6 Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Mon, 14 Sep 2026 09:43:12 +0100 Subject: [PATCH 4/7] Label the query timestamp "Collected At", not "Last Updated" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NinjaOne's spec documents this field as "Date/Time when data was collected/updated" on all 13 Device* query schemas, and the `ts` argument that filters on it as the "Monitoring timestamp filter". It is the collection time — the value collectionWindow.js compares against unixEnd — not the time the underlying record changed. "Last Updated" also sat one character from devices.json's "Last Update" (`lastUpdate`), which is a genuinely different field. The two streams get joined on the same dashboards. "Collected At" uses NinjaOne's own verb and joins the plugin's existing timestamp family: Created At, Updated At, Detected At, Installed At, Started At, Completed At, Closed At. Applied to all eight streams carrying the column, including antivirusStatus, which already had the old label on main, so the set stays consistent. Display label only — the `timestamp` column name and its date shape are unchanged, and no dashboard or doc referenced it. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/NinjaOne/v1/dataStreams/antivirusStatus.json | 2 +- plugins/NinjaOne/v1/dataStreams/antivirusThreats.json | 2 +- plugins/NinjaOne/v1/dataStreams/computerSystems.json | 2 +- plugins/NinjaOne/v1/dataStreams/disksGlobal.json | 2 +- plugins/NinjaOne/v1/dataStreams/operatingSystems.json | 2 +- plugins/NinjaOne/v1/dataStreams/osPatches.json | 2 +- plugins/NinjaOne/v1/dataStreams/processorsGlobal.json | 2 +- plugins/NinjaOne/v1/dataStreams/softwarePatches.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json index f4251fb7..4591c334 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json @@ -74,7 +74,7 @@ }, { "name": "timestamp", - "displayName": "Last Updated", + "displayName": "Collected At", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json index e159c103..18c823f9 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json @@ -100,7 +100,7 @@ }, { "name": "timestamp", - "displayName": "Last Updated", + "displayName": "Collected At", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/computerSystems.json b/plugins/NinjaOne/v1/dataStreams/computerSystems.json index 52c2bf1e..b7ea04ad 100644 --- a/plugins/NinjaOne/v1/dataStreams/computerSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/computerSystems.json @@ -79,7 +79,7 @@ }, { "name": "timestamp", - "displayName": "Last Updated", + "displayName": "Collected At", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json index 8e3bfcd0..9b49fdaa 100644 --- a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json @@ -104,7 +104,7 @@ }, { "name": "timestamp", - "displayName": "Last Updated", + "displayName": "Collected At", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json index bfe22dab..6f70830a 100644 --- a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json @@ -94,7 +94,7 @@ }, { "name": "timestamp", - "displayName": "Last Updated", + "displayName": "Collected At", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/osPatches.json b/plugins/NinjaOne/v1/dataStreams/osPatches.json index 1321e8cb..98b93211 100644 --- a/plugins/NinjaOne/v1/dataStreams/osPatches.json +++ b/plugins/NinjaOne/v1/dataStreams/osPatches.json @@ -95,7 +95,7 @@ }, { "name": "timestamp", - "displayName": "Last Updated", + "displayName": "Collected At", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json index 1b3edfc7..f2128c79 100644 --- a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json @@ -99,7 +99,7 @@ }, { "name": "timestamp", - "displayName": "Last Updated", + "displayName": "Collected At", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json index 8b4ea8af..31a704ad 100644 --- a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json +++ b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json @@ -100,7 +100,7 @@ }, { "name": "timestamp", - "displayName": "Last Updated", + "displayName": "Collected At", "shape": [ "date", { From c515d5bc6296f72e72d382419051514708294c8a Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Mon, 14 Sep 2026 10:03:05 +0100 Subject: [PATCH 5/7] Convert the query epoch timestamps to ISO 8601, and label them "Collection Time" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REVIEW.md:97 is explicit: SquaredUp expects ISO 8601 strings for timestamp columns, and where the upstream API returns Unix timestamps the script must convert them. This plugin already does that in six scripts via the shared convertTimestamps helper — devices.js, locationDevices.js, deviceHealth.js, health.js, backupJobs.js and tickets.js. collectionWindow.js was the outlier. It converts by explicit field list rather than reusing that helper. The helper matches on key substrings and silently misses fields: devices.js leaves `created`, `lastSuccessfulBackupJob` and `lastFailedBackupJob` raw because none of them contain "time", "date", "contact", "update", "start" or "end". Here the full set is known and small — `timestamp`, `detectedAt`, `lastBootTime` and `installedAt`, all typed number/double in NinjaOne's spec and all already carrying a date shape. The conversion runs after the upper-bound filter, which compares against unixEnd in epoch seconds. Inverting that order would break the bound. Also relabels `timestamp` from "Collected At" to "Collection Time" across the eight streams. It is the collection time, not an event on the record, so it does not belong to the plugin's `X At` family (Created At, Detected At, Installed At, Closed At) — those are all things that happened to the record. It still avoids the collision with devices.json's "Last Update" (`lastUpdate`), which was the reason for renaming. Verified on a live tenant: `formatted` output is unchanged from the raw numeric form (09/09/2026 01:45:29), `lastBootTime` now renders as a date, the row with no timestamp still survives at "None", and lastMonth still returns 0 rows, confirming the filter runs before serialisation. Co-Authored-By: Claude Opus 5 (1M context) --- .../v1/dataStreams/antivirusStatus.json | 2 +- .../v1/dataStreams/antivirusThreats.json | 2 +- .../v1/dataStreams/computerSystems.json | 2 +- .../NinjaOne/v1/dataStreams/disksGlobal.json | 2 +- .../v1/dataStreams/operatingSystems.json | 2 +- .../NinjaOne/v1/dataStreams/osPatches.json | 2 +- .../v1/dataStreams/processorsGlobal.json | 2 +- .../dataStreams/scripts/collectionWindow.js | 27 +++++++++++++++++-- .../v1/dataStreams/softwarePatches.json | 2 +- 9 files changed, 33 insertions(+), 10 deletions(-) diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json index 4591c334..3b1b607e 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusStatus.json @@ -74,7 +74,7 @@ }, { "name": "timestamp", - "displayName": "Collected At", + "displayName": "Collection Time", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json index 18c823f9..631279f6 100644 --- a/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json +++ b/plugins/NinjaOne/v1/dataStreams/antivirusThreats.json @@ -100,7 +100,7 @@ }, { "name": "timestamp", - "displayName": "Collected At", + "displayName": "Collection Time", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/computerSystems.json b/plugins/NinjaOne/v1/dataStreams/computerSystems.json index b7ea04ad..8a2ea362 100644 --- a/plugins/NinjaOne/v1/dataStreams/computerSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/computerSystems.json @@ -79,7 +79,7 @@ }, { "name": "timestamp", - "displayName": "Collected At", + "displayName": "Collection Time", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json index 9b49fdaa..fd4b4aac 100644 --- a/plugins/NinjaOne/v1/dataStreams/disksGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/disksGlobal.json @@ -104,7 +104,7 @@ }, { "name": "timestamp", - "displayName": "Collected At", + "displayName": "Collection Time", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json index 6f70830a..d2738977 100644 --- a/plugins/NinjaOne/v1/dataStreams/operatingSystems.json +++ b/plugins/NinjaOne/v1/dataStreams/operatingSystems.json @@ -94,7 +94,7 @@ }, { "name": "timestamp", - "displayName": "Collected At", + "displayName": "Collection Time", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/osPatches.json b/plugins/NinjaOne/v1/dataStreams/osPatches.json index 98b93211..e9247d8b 100644 --- a/plugins/NinjaOne/v1/dataStreams/osPatches.json +++ b/plugins/NinjaOne/v1/dataStreams/osPatches.json @@ -95,7 +95,7 @@ }, { "name": "timestamp", - "displayName": "Collected At", + "displayName": "Collection Time", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json index f2128c79..9cd06c9b 100644 --- a/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json +++ b/plugins/NinjaOne/v1/dataStreams/processorsGlobal.json @@ -99,7 +99,7 @@ }, { "name": "timestamp", - "displayName": "Collected At", + "displayName": "Collection Time", "shape": [ "date", { diff --git a/plugins/NinjaOne/v1/dataStreams/scripts/collectionWindow.js b/plugins/NinjaOne/v1/dataStreams/scripts/collectionWindow.js index e4d07624..b335d201 100644 --- a/plugins/NinjaOne/v1/dataStreams/scripts/collectionWindow.js +++ b/plugins/NinjaOne/v1/dataStreams/scripts/collectionWindow.js @@ -1,4 +1,5 @@ -// Applies the upper end of the timeframe to a /v2/queries/* monitoring snapshot. +// Applies the upper end of the timeframe to a /v2/queries/* monitoring snapshot, and +// serialises the epoch timestamps those endpoints return. // // NinjaOne's `ts` filter accepts exactly one clause - `after X` and `before Y` each work // alone, but `after X and before Y`, `between X and Y` and a repeated `ts` arg all fail @@ -20,6 +21,28 @@ const endTime = tf.enum === 'none' ? null : tf.unixEnd; // Rows with no `timestamp` never reach this filter: whenever a window is selected the // request carries `after unixStart`, which NinjaOne already applies server-side, and it // drops them. The typeof check is belt-and-braces. -result = typeof endTime === 'number' +const kept = typeof endTime === 'number' ? items.filter((item) => typeof item.timestamp === 'number' && item.timestamp <= endTime) : items; + +// Every date column on these endpoints is declared `number`/`double` in NinjaOne's spec, +// so convert to ISO 8601 for the `date` shape. Named rather than key-sniffed: the +// recursive helper the other scripts share matches on substrings and silently misses +// fields (devices.js drops `created` and the backup job dates that way). +// Runs after the filter, which compares against unixEnd in epoch seconds. +const EPOCH_FIELDS = ['timestamp', 'detectedAt', 'lastBootTime', 'installedAt']; + +const toIso = (value) => + typeof value === 'number' && value > 1000000000 && value < 10000000000 + ? new Date(value * 1000).toISOString() + : value; + +result = kept.map((item) => { + const row = { ...item }; + EPOCH_FIELDS.forEach((field) => { + if (field in row) { + row[field] = toIso(row[field]); + } + }); + return row; +}); diff --git a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json index 31a704ad..7c4a6f70 100644 --- a/plugins/NinjaOne/v1/dataStreams/softwarePatches.json +++ b/plugins/NinjaOne/v1/dataStreams/softwarePatches.json @@ -100,7 +100,7 @@ }, { "name": "timestamp", - "displayName": "Collected At", + "displayName": "Collection Time", "shape": [ "date", { From f0f7ec81762cfcf582913411705269d4d0b92cde Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Mon, 14 Sep 2026 10:47:40 +0100 Subject: [PATCH 6/7] Stop activities applying a silent 24-hour window at "None" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `timeframe.start`/`end` still resolve to a default 24-hour window when a tile is set to "None" — they do not go null — so `activities` sent `after`/`before` on a request the user asked to be unfiltered. Guarding on `timeframe.enum === 'none'` omits both arguments instead. This is the same defect already fixed on `softwareGlobal` and `backupJobs` in this PR, and the guard is the same shape as `softwareGlobal`'s. It was originally left on the follow-ups list as "less harmful, an activity log narrowed to 24h still shows rows". That was wrong by two orders of magnitude: on the test tenant a "None" tile returned 36 rows where the unfiltered request returns at least 1000. Also adds `supportsNoneTimeframe`, which `data-streams.md:529` requires alongside `"none"` in a `timeframes` array. `activities` has listed `none` without declaring it since the stream was written. No `defaultTimeframe`: a bounded default is right for an event log, matching `softwareGlobal` and `backupJobs`, which also omit it. Verified live — "None" sends `pageSize=1000` alone and returns 1000; last1hour 0, last24hours 36 (unchanged), last7days 263. Windowed paths still send both bounds. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/NinjaOne/v1/dataStreams/activities.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/NinjaOne/v1/dataStreams/activities.json b/plugins/NinjaOne/v1/dataStreams/activities.json index 2fe1a87c..fe45d2f4 100644 --- a/plugins/NinjaOne/v1/dataStreams/activities.json +++ b/plugins/NinjaOne/v1/dataStreams/activities.json @@ -14,11 +14,11 @@ "getArgs": [ { "key": "after", - "value": "{{timeframe.start}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' ? null : timeframe.start}}" }, { "key": "before", - "value": "{{timeframe.end}}" + "value": "{{typeof timeframe === 'undefined' || timeframe.enum === 'none' ? null : timeframe.end}}" }, { "key": "pageSize", @@ -110,5 +110,6 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true } From eec714b5274af656e88bf9122d1e6d73ea92e89f Mon Sep 17 00:00:00 2001 From: Dan Watts Date: Wed, 16 Sep 2026 08:49:05 +0100 Subject: [PATCH 7/7] Stop tickets applying a silent 24-hour window at "None" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same class of bug as activities/softwareGlobal/backupJobs: tickets.js read the timeframe via mustache substitution into a string and only guarded against substitution failure (NaN), not against "None" — which still resolves unixStart/unixEnd to a default 24-hour window. Switched to the documented context.timeframe global with an explicit enum === 'none' check, and added supportsNoneTimeframe to tickets.json. Not live-verifiable on the test tenant: it has no ticketing add-on, so /v2/ticketing/trigger/board/... has no board objects to test against. Co-Authored-By: Claude Sonnet 5 --- plugins/NinjaOne/v1/dataStreams/scripts/tickets.js | 14 ++++++++------ plugins/NinjaOne/v1/dataStreams/tickets.json | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/plugins/NinjaOne/v1/dataStreams/scripts/tickets.js b/plugins/NinjaOne/v1/dataStreams/scripts/tickets.js index 63a2d7f7..a33cb652 100644 --- a/plugins/NinjaOne/v1/dataStreams/scripts/tickets.js +++ b/plugins/NinjaOne/v1/dataStreams/scripts/tickets.js @@ -1,12 +1,14 @@ // Process NinjaOne ticketing board data const items = (data && data.data && Array.isArray(data.data)) ? data.data : (Array.isArray(data) ? data : []); -// SquaredUp timeframe (Unix seconds). If substitution doesn't happen the -// parseInt yields NaN and hasTimeframe is false — filter is skipped and all -// rows pass through. -const startTime = parseInt('{{timeframe.unixStart}}'); -const endTime = parseInt('{{timeframe.unixEnd}}'); -const hasTimeframe = !isNaN(startTime) && !isNaN(endTime); +// context.timeframe.unixStart/unixEnd still resolve to a default 24-hour window +// when the tile is set to "None", so the enum has to be checked explicitly — +// reading the unix values alone would apply a silent 24-hour filter to a +// request the user asked to be unfiltered. +const tf = context.timeframe || {}; +const hasTimeframe = tf.enum !== 'none' && typeof tf.unixStart === 'number' && typeof tf.unixEnd === 'number'; +const startTime = tf.unixStart; +const endTime = tf.unixEnd; /** * Recursively converts NinjaOne Unix timestamps (seconds) to ISO strings. diff --git a/plugins/NinjaOne/v1/dataStreams/tickets.json b/plugins/NinjaOne/v1/dataStreams/tickets.json index 7f324b8c..084f1e4d 100644 --- a/plugins/NinjaOne/v1/dataStreams/tickets.json +++ b/plugins/NinjaOne/v1/dataStreams/tickets.json @@ -185,5 +185,6 @@ "lastQuarter", "lastYear", "none" - ] + ], + "supportsNoneTimeframe": true }