From 88123f92ae910cad37c7f37ad4e8f07dcc3b2120 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 27 Feb 2026 13:40:29 +0000 Subject: [PATCH 1/8] feat: add work time report view Add a new Work Report view that provides daily work time breakdowns with multi-device support, category filtering, configurable break time (gap merging via flood), and CSV/JSON export. Based on #742 by @ErikBjare. Changes from original: - Implemented thisWeek and thisMonth date ranges - Removed debug console.log statements - Used safeHost consistently in find_bucket queries Closes #742 --- src/components/Header.vue | 4 + src/route.js | 2 + src/views/WorkReport.vue | 325 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+) create mode 100644 src/views/WorkReport.vue diff --git a/src/components/Header.vue b/src/components/Header.vue index 9a4a7827..c7f37db3 100644 --- a/src/components/Header.vue +++ b/src/components/Header.vue @@ -59,6 +59,9 @@ div(:class="{'fixed-top-padding': fixedTopMenu}") b-dropdown-item(to="/search") icon(name="search") | Search + b-dropdown-item(to="/work-report") + icon(name="briefcase") + | Work Report b-dropdown-item(to="/trends" v-if="devmode") icon(name="chart-line") | Trends @@ -98,6 +101,7 @@ div(:class="{'fixed-top-padding': fixedTopMenu}") + + From 1292c98166529aabaede2362118ba78241830a30 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 31 Mar 2026 03:02:22 +0000 Subject: [PATCH 2/8] fix(WorkReport): use original hostname in find_bucket, safeHost only for variable names Bucket IDs preserve the original hostname (e.g. 'aw-watcher-window_my-laptop'), but variable names in the query language must be alphanumeric. The sanitized safeHost is correct for variable naming, but find_bucket must use the original hostname to locate the bucket. --- src/views/WorkReport.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/WorkReport.vue b/src/views/WorkReport.vue index 8832673d..80a5d608 100644 --- a/src/views/WorkReport.vue +++ b/src/views/WorkReport.vue @@ -181,7 +181,7 @@ export default { for (const hostname of this.selectedHosts) { const safeHost = hostname.replace(/[^a-zA-Z0-9_]/g, ''); query += ` - events_${safeHost} = flood(query_bucket(find_bucket("aw-watcher-window_${safeHost}")), ${breakTimeSeconds}); + events_${safeHost} = flood(query_bucket(find_bucket("aw-watcher-window_${hostname}")), ${breakTimeSeconds}); events_${safeHost} = categorize(events_${safeHost}, ${categoriesStr}); events_${safeHost} = filter_keyvals(events_${safeHost}, "$category", ${JSON.stringify( categoriesFilter From 9014e9f18329fcc77ab279b0507866c64c13e474 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 31 Mar 2026 03:13:43 +0000 Subject: [PATCH 3/8] fix(WorkReport): fix startOfDay offset and add AFK filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two P1 bugs flagged by Greptile: 1. startOfDay offset was silently ignored — moment().add('04:00') is a no-op (moment expects (amount, unit) or ISO 8601). Now uses get_day_start_with_offset/get_day_end_with_offset from util/time, the same helpers used by every other view. 2. AFK time was counted as work time — the per-host query never intersected with aw-watcher-afk data. Now applies the standard not_afk pattern (flood → filter_keyvals status=not-afk → filter_period_intersect) before categorizing events, matching the canonicalEvents pattern in src/queries.ts. --- src/views/WorkReport.vue | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/views/WorkReport.vue b/src/views/WorkReport.vue index 80a5d608..11a54fd3 100644 --- a/src/views/WorkReport.vue +++ b/src/views/WorkReport.vue @@ -75,6 +75,7 @@ import { getClient } from '~/util/awclient'; import { useCategoryStore } from '~/stores/categories'; import { useSettingsStore } from '~/stores/settings'; import { useBucketsStore } from '~/stores/buckets'; +import { get_day_start_with_offset, get_day_end_with_offset } from '~/util/time'; import 'vue-awesome/icons/sync'; import 'vue-awesome/icons/download'; @@ -175,13 +176,16 @@ export default { const categories = this.categoryStore.classes_for_query; const categoriesStr = JSON.stringify(categories).replace(/\\\\/g, '\\'); - // Build multi-device query with flood-based gap merging + // Build multi-device query with flood-based gap merging and AFK filtering let query = ''; for (const hostname of this.selectedHosts) { const safeHost = hostname.replace(/[^a-zA-Z0-9_]/g, ''); query += ` events_${safeHost} = flood(query_bucket(find_bucket("aw-watcher-window_${hostname}")), ${breakTimeSeconds}); + not_afk_${safeHost} = flood(query_bucket(find_bucket("aw-watcher-afk_${hostname}"))); + not_afk_${safeHost} = filter_keyvals(not_afk_${safeHost}, "status", ["not-afk"]); + events_${safeHost} = filter_period_intersect(events_${safeHost}, not_afk_${safeHost}); events_${safeHost} = categorize(events_${safeHost}, ${categoriesStr}); events_${safeHost} = filter_keyvals(events_${safeHost}, "$category", ${JSON.stringify( categoriesFilter @@ -247,9 +251,10 @@ export default { } for (let i = days - 1; i >= 0; i--) { - const start = moment().subtract(i, 'days').startOf('day').add(offset); - const end = start.clone().add(1, 'day'); - timeperiods.push(start.format() + '/' + end.format()); + const date = moment().subtract(i, 'days'); + const start = get_day_start_with_offset(date, offset); + const end = get_day_end_with_offset(date, offset); + timeperiods.push(start + '/' + end); } return timeperiods; From 30f2fa77a831ddb7cc819e17ebd5156c83cfa645 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 1 Apr 2026 17:32:28 +0000 Subject: [PATCH 4/8] fix(WorkReport): use query_bucket directly and safeHostname from queries.ts --- src/queries.ts | 2 +- src/views/WorkReport.vue | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/queries.ts b/src/queries.ts index 816b6cab..406a3431 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -15,7 +15,7 @@ function escape_doublequote(s: string) { } // Hostname safe for using as a variable name -function safeHostname(hostname: string): string { +export function safeHostname(hostname: string): string { return hostname.replace(/[^a-zA-Z0-9_]/g, ''); } diff --git a/src/views/WorkReport.vue b/src/views/WorkReport.vue index 11a54fd3..0b895db6 100644 --- a/src/views/WorkReport.vue +++ b/src/views/WorkReport.vue @@ -76,6 +76,7 @@ import { useCategoryStore } from '~/stores/categories'; import { useSettingsStore } from '~/stores/settings'; import { useBucketsStore } from '~/stores/buckets'; import { get_day_start_with_offset, get_day_end_with_offset } from '~/util/time'; +import { safeHostname } from '~/queries'; import 'vue-awesome/icons/sync'; import 'vue-awesome/icons/download'; @@ -180,10 +181,10 @@ export default { let query = ''; for (const hostname of this.selectedHosts) { - const safeHost = hostname.replace(/[^a-zA-Z0-9_]/g, ''); + const safeHost = safeHostname(hostname); query += ` - events_${safeHost} = flood(query_bucket(find_bucket("aw-watcher-window_${hostname}")), ${breakTimeSeconds}); - not_afk_${safeHost} = flood(query_bucket(find_bucket("aw-watcher-afk_${hostname}"))); + events_${safeHost} = flood(query_bucket("aw-watcher-window_${hostname}"), ${breakTimeSeconds}); + not_afk_${safeHost} = flood(query_bucket("aw-watcher-afk_${hostname}")); not_afk_${safeHost} = filter_keyvals(not_afk_${safeHost}, "status", ["not-afk"]); events_${safeHost} = filter_period_intersect(events_${safeHost}, not_afk_${safeHost}); events_${safeHost} = categorize(events_${safeHost}, ${categoriesStr}); @@ -196,7 +197,7 @@ export default { // Combine events from all hosts query += '\nevents = [];'; for (const hostname of this.selectedHosts) { - const safeHost = hostname.replace(/[^a-zA-Z0-9_]/g, ''); + const safeHost = safeHostname(hostname); query += `\nevents = union_no_overlap(events, events_${safeHost});`; } From 36b454a6fe8a576ee4a6025bf612e17430aa4127 Mon Sep 17 00:00:00 2001 From: Bob Date: Sun, 5 Apr 2026 01:05:23 +0000 Subject: [PATCH 5/8] fix(WorkReport): add empty category guard and fix hostname collision - Add validation: alert when no categories selected (previously silent all-zeros) - Use indexed AQL variables (events_0, events_1, ...) instead of safeHostname() to prevent collisions when hostnames differ only in non-alphanumeric chars (e.g., 'my-laptop' vs 'mylaptop') - Remove unused safeHostname import Addresses Greptile review findings on PR #775. --- src/views/WorkReport.vue | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/views/WorkReport.vue b/src/views/WorkReport.vue index 0b895db6..96209663 100644 --- a/src/views/WorkReport.vue +++ b/src/views/WorkReport.vue @@ -76,7 +76,6 @@ import { useCategoryStore } from '~/stores/categories'; import { useSettingsStore } from '~/stores/settings'; import { useBucketsStore } from '~/stores/buckets'; import { get_day_start_with_offset, get_day_end_with_offset } from '~/util/time'; -import { safeHostname } from '~/queries'; import 'vue-awesome/icons/sync'; import 'vue-awesome/icons/download'; @@ -170,6 +169,12 @@ export default { return; } + if (this.selectedCategories.length === 0) { + alert('Please select at least one category'); + this.loading = false; + return; + } + const timeperiods = this.getTimeperiods(); const breakTimeSeconds = this.breakTime * 60; const categoriesFilter = this.selectedCategories.map(c => JSON.parse(c)); @@ -180,15 +185,17 @@ export default { // Build multi-device query with flood-based gap merging and AFK filtering let query = ''; - for (const hostname of this.selectedHosts) { - const safeHost = safeHostname(hostname); + // Use indexed variable names to avoid collisions when hostnames + // differ only in non-alphanumeric chars (e.g., "my-laptop" vs "mylaptop"). + for (let hi = 0; hi < this.selectedHosts.length; hi++) { + const hostname = this.selectedHosts[hi]; query += ` - events_${safeHost} = flood(query_bucket("aw-watcher-window_${hostname}"), ${breakTimeSeconds}); - not_afk_${safeHost} = flood(query_bucket("aw-watcher-afk_${hostname}")); - not_afk_${safeHost} = filter_keyvals(not_afk_${safeHost}, "status", ["not-afk"]); - events_${safeHost} = filter_period_intersect(events_${safeHost}, not_afk_${safeHost}); - events_${safeHost} = categorize(events_${safeHost}, ${categoriesStr}); - events_${safeHost} = filter_keyvals(events_${safeHost}, "$category", ${JSON.stringify( + events_${hi} = flood(query_bucket("aw-watcher-window_${hostname}"), ${breakTimeSeconds}); + not_afk_${hi} = flood(query_bucket("aw-watcher-afk_${hostname}")); + not_afk_${hi} = filter_keyvals(not_afk_${hi}, "status", ["not-afk"]); + events_${hi} = filter_period_intersect(events_${hi}, not_afk_${hi}); + events_${hi} = categorize(events_${hi}, ${categoriesStr}); + events_${hi} = filter_keyvals(events_${hi}, "$category", ${JSON.stringify( categoriesFilter )}); `; @@ -196,9 +203,8 @@ export default { // Combine events from all hosts query += '\nevents = [];'; - for (const hostname of this.selectedHosts) { - const safeHost = safeHostname(hostname); - query += `\nevents = union_no_overlap(events, events_${safeHost});`; + for (let hi = 0; hi < this.selectedHosts.length; hi++) { + query += `\nevents = union_no_overlap(events, events_${hi});`; } query += ` From 024468ad42ee2733f0926ab79806086bb76b9fcc Mon Sep 17 00:00:00 2001 From: Bob Date: Sun, 5 Apr 2026 15:22:20 +0000 Subject: [PATCH 6/8] fix(work-report): guard hosts without AFK buckets --- src/util/workReport.ts | 42 +++++++++++++++++++++++++++++++ src/views/WorkReport.vue | 29 ++++++++++++--------- test/unit/workReport.test.node.ts | 38 ++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 src/util/workReport.ts create mode 100644 test/unit/workReport.test.node.ts diff --git a/src/util/workReport.ts b/src/util/workReport.ts new file mode 100644 index 00000000..73d3f2e1 --- /dev/null +++ b/src/util/workReport.ts @@ -0,0 +1,42 @@ +import { IBucket } from '~/util/interfaces'; + +export interface WorkReportHostOption { + value: string; + text: string; + disabled: boolean; +} + +function getWindowHosts(buckets: IBucket[]): string[] { + const hosts = buckets + .filter(bucket => bucket.type === 'currentwindow') + .map(bucket => bucket.id.replace('aw-watcher-window_', '')); + return [...new Set(hosts)]; +} + +function getAFKHosts(buckets: IBucket[]): Set { + return new Set( + buckets + .filter(bucket => bucket.type === 'afkstatus') + .map(bucket => bucket.id.replace('aw-watcher-afk_', '')) + ); +} + +export function getWorkReportHostOptions(buckets: IBucket[]): WorkReportHostOption[] { + const afkHosts = getAFKHosts(buckets); + return getWindowHosts(buckets).map(host => { + const hasAFK = afkHosts.has(host); + return { + value: host, + text: hasAFK ? host : `${host} (requires aw-watcher-afk)`, + disabled: !hasAFK, + }; + }); +} + +export function getUnsupportedWorkReportHosts( + selectedHosts: string[], + buckets: IBucket[] +): string[] { + const afkHosts = getAFKHosts(buckets); + return selectedHosts.filter(host => !afkHosts.has(host)); +} diff --git a/src/views/WorkReport.vue b/src/views/WorkReport.vue index 96209663..ffe0e0de 100644 --- a/src/views/WorkReport.vue +++ b/src/views/WorkReport.vue @@ -76,6 +76,7 @@ import { useCategoryStore } from '~/stores/categories'; import { useSettingsStore } from '~/stores/settings'; import { useBucketsStore } from '~/stores/buckets'; import { get_day_start_with_offset, get_day_end_with_offset } from '~/util/time'; +import { getWorkReportHostOptions, getUnsupportedWorkReportHosts } from '~/util/workReport'; import 'vue-awesome/icons/sync'; import 'vue-awesome/icons/download'; @@ -108,17 +109,7 @@ export default { }, computed: { hostOptions() { - const allBuckets = this.bucketsStore.buckets || []; - const windowBuckets = allBuckets.filter(b => b.type === 'currentwindow'); - - const hosts = windowBuckets.map(b => { - return b.id.replace('aw-watcher-window_', ''); - }); - - return hosts.map(host => ({ - value: host, - text: host, - })); + return getWorkReportHostOptions(this.bucketsStore.buckets || []); }, categoryOptions() { @@ -154,7 +145,7 @@ export default { await this.bucketsStore.ensureLoaded(); if (this.hostOptions.length > 0) { - this.selectedHosts = this.hostOptions.map(opt => opt.value); + this.selectedHosts = this.hostOptions.filter(opt => !opt.disabled).map(opt => opt.value); } }, methods: { @@ -175,6 +166,20 @@ export default { return; } + const unsupportedHosts = getUnsupportedWorkReportHosts( + this.selectedHosts, + this.bucketsStore.buckets || [] + ); + if (unsupportedHosts.length > 0) { + alert( + `The following hosts are missing aw-watcher-afk buckets and can't be included in Work Report: ${unsupportedHosts.join( + ', ' + )}` + ); + this.loading = false; + return; + } + const timeperiods = this.getTimeperiods(); const breakTimeSeconds = this.breakTime * 60; const categoriesFilter = this.selectedCategories.map(c => JSON.parse(c)); diff --git a/test/unit/workReport.test.node.ts b/test/unit/workReport.test.node.ts new file mode 100644 index 00000000..8c8d364f --- /dev/null +++ b/test/unit/workReport.test.node.ts @@ -0,0 +1,38 @@ +import { getUnsupportedWorkReportHosts, getWorkReportHostOptions } from '~/util/workReport'; + +const buckets = [ + { + id: 'aw-watcher-window_laptop', + hostname: 'laptop', + device_id: 'laptop', + type: 'currentwindow', + data: {}, + }, + { + id: 'aw-watcher-afk_laptop', + hostname: 'laptop', + device_id: 'laptop', + type: 'afkstatus', + data: {}, + }, + { + id: 'aw-watcher-window_phone', + hostname: 'phone', + device_id: 'phone', + type: 'currentwindow', + data: {}, + }, +]; + +describe('workReport host helpers', () => { + test('getWorkReportHostOptions disables hosts without AFK buckets', () => { + expect(getWorkReportHostOptions(buckets as any)).toEqual([ + { value: 'laptop', text: 'laptop', disabled: false }, + { value: 'phone', text: 'phone (requires aw-watcher-afk)', disabled: true }, + ]); + }); + + test('getUnsupportedWorkReportHosts returns selected hosts missing AFK buckets', () => { + expect(getUnsupportedWorkReportHosts(['laptop', 'phone'], buckets as any)).toEqual(['phone']); + }); +}); From 58760d9ca071b321b831f5c740faf4761ffada57 Mon Sep 17 00:00:00 2001 From: Bob Date: Sun, 5 Apr 2026 18:19:46 +0000 Subject: [PATCH 7/8] test(router): cover work report route and landingpage redirect --- test/unit/route.test.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test/unit/route.test.js diff --git a/test/unit/route.test.js b/test/unit/route.test.js new file mode 100644 index 00000000..b9360c01 --- /dev/null +++ b/test/unit/route.test.js @@ -0,0 +1,24 @@ +import router from '~/route'; + +describe('router', () => { + test('root redirect uses localStorage landingpage when set', () => { + const rootRoute = router.options.routes.find(route => route.path === '/'); + + localStorage.landingpage = '/work-report'; + expect(rootRoute.redirect({})).toBe('/work-report'); + }); + + test('root redirect falls back to /home when localStorage landingpage is missing', () => { + const rootRoute = router.options.routes.find(route => route.path === '/'); + + delete localStorage.landingpage; + expect(rootRoute.redirect({})).toBe('/home'); + }); + + test('includes the work report route', () => { + const workReportRoute = router.options.routes.find(route => route.path === '/work-report'); + + expect(workReportRoute).toBeTruthy(); + expect(typeof workReportRoute.component).toBe('function'); + }); +}); From eed8ff0ee5dd8373e8e66d38ca00d56e192c76f5 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 6 Apr 2026 03:22:26 +0000 Subject: [PATCH 8/8] fix(work-report): skip hosts without AFK buckets --- src/util/workReport.ts | 5 +++++ src/views/WorkReport.vue | 37 ++++++++++++++++++++++++------- test/unit/workReport.test.node.ts | 34 +++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/util/workReport.ts b/src/util/workReport.ts index 73d3f2e1..15f58f94 100644 --- a/src/util/workReport.ts +++ b/src/util/workReport.ts @@ -40,3 +40,8 @@ export function getUnsupportedWorkReportHosts( const afkHosts = getAFKHosts(buckets); return selectedHosts.filter(host => !afkHosts.has(host)); } + +export function getSupportedWorkReportHosts(selectedHosts: string[], buckets: IBucket[]): string[] { + const unsupportedHosts = new Set(getUnsupportedWorkReportHosts(selectedHosts, buckets)); + return selectedHosts.filter(host => !unsupportedHosts.has(host)); +} diff --git a/src/views/WorkReport.vue b/src/views/WorkReport.vue index ffe0e0de..0c51e27e 100644 --- a/src/views/WorkReport.vue +++ b/src/views/WorkReport.vue @@ -76,7 +76,11 @@ import { useCategoryStore } from '~/stores/categories'; import { useSettingsStore } from '~/stores/settings'; import { useBucketsStore } from '~/stores/buckets'; import { get_day_start_with_offset, get_day_end_with_offset } from '~/util/time'; -import { getWorkReportHostOptions, getUnsupportedWorkReportHosts } from '~/util/workReport'; +import { + getSupportedWorkReportHosts, + getWorkReportHostOptions, + getUnsupportedWorkReportHosts, +} from '~/util/workReport'; import 'vue-awesome/icons/sync'; import 'vue-awesome/icons/download'; @@ -171,15 +175,32 @@ export default { this.bucketsStore.buckets || [] ); if (unsupportedHosts.length > 0) { + const supportedHosts = getSupportedWorkReportHosts( + this.selectedHosts, + this.bucketsStore.buckets || [] + ); + if (supportedHosts.length === 0) { + alert( + `The selected hosts are missing aw-watcher-afk buckets and can't be included in Work Report: ${unsupportedHosts.join( + ', ' + )}` + ); + this.loading = false; + return; + } + alert( - `The following hosts are missing aw-watcher-afk buckets and can't be included in Work Report: ${unsupportedHosts.join( + `Skipping hosts without aw-watcher-afk buckets: ${unsupportedHosts.join( ', ' - )}` + )}. Work Report will use: ${supportedHosts.join(', ')}` ); - this.loading = false; - return; + this.selectedHosts = supportedHosts; } + const hostsToQuery = getSupportedWorkReportHosts( + this.selectedHosts, + this.bucketsStore.buckets || [] + ); const timeperiods = this.getTimeperiods(); const breakTimeSeconds = this.breakTime * 60; const categoriesFilter = this.selectedCategories.map(c => JSON.parse(c)); @@ -192,8 +213,8 @@ export default { // Use indexed variable names to avoid collisions when hostnames // differ only in non-alphanumeric chars (e.g., "my-laptop" vs "mylaptop"). - for (let hi = 0; hi < this.selectedHosts.length; hi++) { - const hostname = this.selectedHosts[hi]; + for (let hi = 0; hi < hostsToQuery.length; hi++) { + const hostname = hostsToQuery[hi]; query += ` events_${hi} = flood(query_bucket("aw-watcher-window_${hostname}"), ${breakTimeSeconds}); not_afk_${hi} = flood(query_bucket("aw-watcher-afk_${hostname}")); @@ -208,7 +229,7 @@ export default { // Combine events from all hosts query += '\nevents = [];'; - for (let hi = 0; hi < this.selectedHosts.length; hi++) { + for (let hi = 0; hi < hostsToQuery.length; hi++) { query += `\nevents = union_no_overlap(events, events_${hi});`; } diff --git a/test/unit/workReport.test.node.ts b/test/unit/workReport.test.node.ts index 8c8d364f..a79e4203 100644 --- a/test/unit/workReport.test.node.ts +++ b/test/unit/workReport.test.node.ts @@ -1,4 +1,8 @@ -import { getUnsupportedWorkReportHosts, getWorkReportHostOptions } from '~/util/workReport'; +import { + getSupportedWorkReportHosts, + getUnsupportedWorkReportHosts, + getWorkReportHostOptions, +} from '~/util/workReport'; const buckets = [ { @@ -35,4 +39,32 @@ describe('workReport host helpers', () => { test('getUnsupportedWorkReportHosts returns selected hosts missing AFK buckets', () => { expect(getUnsupportedWorkReportHosts(['laptop', 'phone'], buckets as any)).toEqual(['phone']); }); + + test('getSupportedWorkReportHosts returns only hosts with AFK buckets', () => { + expect(getSupportedWorkReportHosts(['laptop', 'phone'], buckets as any)).toEqual(['laptop']); + }); + + test('getSupportedWorkReportHosts preserves selected host order', () => { + const moreBuckets = [ + ...buckets, + { + id: 'aw-watcher-window_desktop', + hostname: 'desktop', + device_id: 'desktop', + type: 'currentwindow', + data: {}, + }, + { + id: 'aw-watcher-afk_desktop', + hostname: 'desktop', + device_id: 'desktop', + type: 'afkstatus', + data: {}, + }, + ]; + + expect(getSupportedWorkReportHosts(['desktop', 'phone', 'laptop'], moreBuckets as any)).toEqual( + ['desktop', 'laptop'] + ); + }); });