From 012ea25c5296e5aa0bd062513bf5c6e1130b35e0 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 28 Aug 2026 06:17:19 +0000 Subject: [PATCH 1/6] fix(queries): match Dia/Arc chrome forks and surface empty Browser view Chromium forks run the chrome extension, so web events land in the chrome bucket while aw-watcher-window reports app names like "Dia". Nothing in the chrome patterns matched, so filter_period_intersect returned empty and Top Domains/URLs/Titles silently showed "No data". Add Dia/Arc process-name alternatives to the chrome regex, the Dia macOS bundle id to the exact list, and an info hint when a browser bucket exists but the window intersection is empty. Fixes ActivityWatch/aw-webui#927. Git-Session-Id: 47fd40d1-68e2-5710-b21c-e61a21b8e5cd --- src/components/SelectableVisualization.vue | 9 ++++++ src/queries.ts | 13 +++++++-- src/util/browserAllowlist.ts | 21 ++++++++++++++ test/unit/browserAllowlist.test.node.ts | 23 +++++++++++++++ test/unit/queries.test.node.ts | 33 ++++++++++++++++++++-- 5 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/util/browserAllowlist.ts create mode 100644 test/unit/browserAllowlist.test.node.ts diff --git a/src/components/SelectableVisualization.vue b/src/components/SelectableVisualization.vue index 8d5b225ce..9da49b5c8 100644 --- a/src/components/SelectableVisualization.vue +++ b/src/components/SelectableVisualization.vue @@ -38,6 +38,8 @@ div(v-if="editable || !activityStore.buckets.loaded || has_prerequisites || !set :namefunc="e => e.data.classname", :colorfunc="e => e.data.app", with_limit) + b-alert.small.px-2.py-1(v-if="isBrowserVis && browserAllowlistMiss" show variant="info") + | No matching browser window for this period. If you were browsing in a Chromium/Firefox fork, its app name may not be recognized yet (see #[a(href="https://github.com/ActivityWatch/aw-webui/issues/927") #927]). div(v-if="type == 'top_domains'") aw-summary(:fields="activityStore.browser.top_domains", :namefunc="e => e.data.$domain", @@ -132,6 +134,7 @@ import { useCategoryStore } from '~/stores/categories'; import { useBucketsStore } from '~/stores/buckets'; import { useViewsStore } from '~/stores/views'; import { useSettingsStore } from '~/stores/settings'; +import { isBrowserAllowlistMiss } from '~/util/browserAllowlist'; import moment from 'moment'; @@ -282,6 +285,12 @@ export default { has_prerequisites() { return this.visualizations[this.type].available; }, + isBrowserVis() { + return ['top_domains', 'top_urls', 'top_browser_titles'].includes(this.type); + }, + browserAllowlistMiss() { + return isBrowserAllowlistMiss(this.activityStore.browser); + }, supports_period: function () { if (this.type == 'sunburst_clock' || this.type == 'vis_timeline') { return this.isSingleDay; diff --git a/src/queries.ts b/src/queries.ts index 5a4763dae..cd30aa320 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -277,8 +277,16 @@ export function appQuery( // variants (upper/lowercase, spacing, .exe suffix) are handled by // browser_appname_regex using (?i) flag. See test/unit/queries.test.node.ts for // the complete list of known app names these patterns cover. -const browser_appnames: Record = { - chrome: ['com.google.Chrome', 'com.google.ChromeDev', 'org.chromium.Chromium'], +export const browser_appnames: Record = { + // Chromium forks (Dia, Arc) run the chrome extension by default, so their + // web events land in the chrome bucket. Reverse-domain identifiers don't + // match the process-name regex below and have to live here (#927). + chrome: [ + 'com.google.Chrome', + 'com.google.ChromeDev', + 'org.chromium.Chromium', + 'company.thebrowser.dia', + ], firefox: ['org.mozilla.firefox', 'io.gitlab.librewolf-community', 'net.waterfox.waterfox'], opera: ['com.opera.Opera'], brave: ['com.brave.Browser'], @@ -316,6 +324,7 @@ export const browser_appname_regex: Record = { // default their events land in the chrome bucket and their app names have to be matched // here (#927, ActivityWatch/activitywatch#1094). The standalone arc key below only covers // setups where Arc was picked explicitly in the settings, which changes the bucket name. + // Fork alternatives are $-anchored so names like "archive" / "Dialog" don't match. chrome: '(?i)^(google[-_ ]?chrome|chrome|chromium|arc(\\.exe)?$|dia(\\.exe)?$)', firefox: '(?i)(firefox|librewolf|waterfox|nightly)', opera: '(?i)(opera)', diff --git a/src/util/browserAllowlist.ts b/src/util/browserAllowlist.ts new file mode 100644 index 000000000..2dba09cd7 --- /dev/null +++ b/src/util/browserAllowlist.ts @@ -0,0 +1,21 @@ +/** + * Detect the silent-empty Browser view: a browser watcher bucket exists, the + * query finished, and the window-event intersection came back empty. + * + * This is the Chromium-fork failure mode (aw-webui#927): events land in + * `aw-watcher-web-chrome_*` but `app` is "Dia"/"Arc"/… and matches nothing. + * It is also the honest "didn't browse this period" case, so the UI copy + * must cover both. + * + * `top_domains === null` means the query is still in flight (see + * `start_loading` in the activity store) and must not fire the hint. + */ +export function isBrowserAllowlistMiss(browser: { + available: boolean; + duration: number; + top_domains: unknown[] | null; +}): boolean { + if (!browser.available) return false; + if (browser.top_domains === null) return false; + return browser.duration === 0 && browser.top_domains.length === 0; +} diff --git a/test/unit/browserAllowlist.test.node.ts b/test/unit/browserAllowlist.test.node.ts new file mode 100644 index 000000000..6570f13dd --- /dev/null +++ b/test/unit/browserAllowlist.test.node.ts @@ -0,0 +1,23 @@ +import { isBrowserAllowlistMiss } from '~/util/browserAllowlist'; + +describe('isBrowserAllowlistMiss', () => { + const empty = { available: true, duration: 0, top_domains: [] as unknown[] }; + + test('true when a browser bucket exists but the window intersection is empty', () => { + expect(isBrowserAllowlistMiss(empty)).toBe(true); + }); + + test('false while the query is still in flight (null fields)', () => { + expect(isBrowserAllowlistMiss({ ...empty, top_domains: null })).toBe(false); + }); + + test('false when no browser watcher bucket is present', () => { + expect(isBrowserAllowlistMiss({ ...empty, available: false })).toBe(false); + }); + + test('false when matched browser events exist', () => { + expect(isBrowserAllowlistMiss({ available: true, duration: 12, top_domains: [{}] })).toBe( + false + ); + }); +}); diff --git a/test/unit/queries.test.node.ts b/test/unit/queries.test.node.ts index e6c4508cc..2adf8c6bb 100644 --- a/test/unit/queries.test.node.ts +++ b/test/unit/queries.test.node.ts @@ -14,6 +14,9 @@ * 'Google-chrome-beta', 'Google-chrome-unstable' * (Flatpak app IDs retained as exact: 'com.google.Chrome', 'com.google.ChromeDev', * 'org.chromium.Chromium') + * Chromium forks that report through the chrome extension bucket (#927): + * 'Arc', 'arc.exe', 'Arc.exe', 'Dia', 'Dia.exe' + * (macOS bundle ID retained as exact: 'company.thebrowser.dia') * * Firefox: 'Firefox', 'Firefox.exe', 'firefox', 'firefox.exe', * 'Firefox Developer Edition', 'firefoxdeveloperedition', @@ -56,11 +59,13 @@ */ import { - browser_appname_regex, appQuery, + browser_appname_regex, + browser_appnames, + canonicalEvents, categoryQuery, + fullDesktopQuery, querystr_to_array, - canonicalEvents, } from '~/queries'; // Convert ActivityWatch (?i) patterns to JS RegExp with i flag for testing. @@ -103,8 +108,9 @@ describe('browser_appname_regex', () => { test('chrome pattern does not false-positive', () => { const re = toRegex(browser_appname_regex.chrome); - // Flatpak app IDs are in the exact list, not matched by regex + // Flatpak / bundle IDs are in the exact list, not matched by regex expect(re.test('com.google.Chrome')).toBe(false); + expect(re.test('company.thebrowser.dia')).toBe(false); expect(re.test('Slack')).toBe(false); expect(re.test('Electron')).toBe(false); // The fork alternatives are anchored, so names merely starting with them don't match @@ -113,6 +119,10 @@ describe('browser_appname_regex', () => { expect(re.test('Dialog')).toBe(false); }); + test('chrome exact list includes the Dia macOS bundle id', () => { + expect(browser_appnames.chrome).toContain('company.thebrowser.dia'); + }); + test('firefox pattern matches all known Firefox/LibreWolf/Waterfox app names', () => { const re = toRegex(browser_appname_regex.firefox); // Every entry from the old exact-match list @@ -250,6 +260,23 @@ describe('browser_appname_regex', () => { }); }); +describe('chrome fork matching in generated query', () => { + test('chrome bucket query includes Dia bundle id and process-name regex', () => { + const query = fullDesktopQuery({ + bid_window: 'aw-watcher-window_testhost', + bid_afk: 'aw-watcher-afk_testhost', + bid_browsers: ['aw-watcher-web-chrome_testhost'], + filter_afk: true, + include_audible: false, + categories: [], + filter_categories: [], + }).join('\n'); + expect(query).toContain('company.thebrowser.dia'); + // JSON.stringify doubles the regex backslash, so the query text has \\. + expect(query).toContain('dia(\\\\.exe)?$'); + }); +}); + describe('querystr_to_array', () => { test('splits simple multi-statement query correctly', () => { const query = 'events = query_bucket("aw-watcher-window_host"); RETURN = {"events": events};'; From 923e114cc2e0110f656d012627de9b2374770bc1 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 28 Aug 2026 06:30:14 +0000 Subject: [PATCH 2/6] fix(queries): avoid duplicate Arc browser events Git-Session-Id: 47fd40d1-68e2-5710-b21c-e61a21b8e5cd --- src/components/SelectableVisualization.vue | 3 ++- src/i18n/locales/de.ts | 4 +++ src/i18n/locales/en.ts | 4 +++ src/i18n/locales/ru.ts | 4 +++ src/i18n/locales/uk.ts | 4 +++ src/i18n/locales/zh-CN.ts | 4 +++ src/queries.ts | 15 ++++++++--- test/unit/queries.test.node.ts | 31 +++++++++++++++++----- 8 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/components/SelectableVisualization.vue b/src/components/SelectableVisualization.vue index 9da49b5c8..5da6772cd 100644 --- a/src/components/SelectableVisualization.vue +++ b/src/components/SelectableVisualization.vue @@ -39,7 +39,8 @@ div(v-if="editable || !activityStore.buckets.loaded || has_prerequisites || !set :colorfunc="e => e.data.app", with_limit) b-alert.small.px-2.py-1(v-if="isBrowserVis && browserAllowlistMiss" show variant="info") - | No matching browser window for this period. If you were browsing in a Chromium/Firefox fork, its app name may not be recognized yet (see #[a(href="https://github.com/ActivityWatch/aw-webui/issues/927") #927]). + | {{ $t('activity.browserAllowlistMiss') }} + | (#[a(href="https://github.com/ActivityWatch/aw-webui/issues/927") #927]) div(v-if="type == 'top_domains'") aw-summary(:fields="activityStore.browser.top_domains", :namefunc="e => e.data.$domain", diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index ef9fa3817..bbc01b451 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -61,6 +61,10 @@ export default { title: 'Hoppla, diese Seite wurde nicht gefunden!', hint: 'Versuchen Sie, dorthin zurückzukehren, woher Sie kamen.', }, + activity: { + browserAllowlistMiss: + 'Für diesen Zeitraum wurde kein passendes Browserfenster gefunden. Falls Sie in einem Chromium-/Firefox-Derivat gesurft haben, wird dessen App-Name möglicherweise noch nicht erkannt.', + }, settings: { title: 'Einstellungen', unsavedCategoriesLeave: 'Ihre Kategorien haben ungespeicherte Änderungen. Wirklich verlassen?', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index cced80c7c..ba8d470a9 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -62,6 +62,10 @@ export default { title: 'Oops, this page was not found!', hint: 'Try navigating back where you came from.', }, + activity: { + browserAllowlistMiss: + 'No matching browser window for this period. If you were browsing in a Chromium/Firefox fork, its app name may not be recognized yet.', + }, settings: { title: 'Settings', unsavedCategoriesLeave: 'Your categories have unsaved changes, are you sure you want to leave?', diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 9eac8ae2a..a7473b4ea 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -60,6 +60,10 @@ export default { title: 'Ой, эта страница не найдена!', hint: 'Попробуйте вернуться туда, откуда пришли.', }, + activity: { + browserAllowlistMiss: + 'Для этого периода не найдено подходящее окно браузера. Если вы использовали форк Chromium/Firefox, имя его приложения может пока не распознаваться.', + }, settings: { title: 'Настройки', unsavedCategoriesLeave: 'В категориях есть несохранённые изменения. Действительно выйти?', diff --git a/src/i18n/locales/uk.ts b/src/i18n/locales/uk.ts index 86808d45a..19b780356 100644 --- a/src/i18n/locales/uk.ts +++ b/src/i18n/locales/uk.ts @@ -61,6 +61,10 @@ export default { title: 'Ой, цю сторінку не знайдено!', hint: 'Спробуйте повернутися туди, звідки прийшли.', }, + activity: { + browserAllowlistMiss: + 'Для цього періоду не знайдено відповідного вікна браузера. Якщо ви користувалися форком Chromium/Firefox, назва його застосунку може ще не розпізнаватися.', + }, settings: { title: 'Налаштування', unsavedCategoriesLeave: 'У категоріях є незбережені зміни. Справді вийти?', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 87a275101..8c28d8353 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -61,6 +61,10 @@ export default { title: '页面未找到!', hint: '请尝试返回之前所在的页面。', }, + activity: { + browserAllowlistMiss: + '此时间段内未找到匹配的浏览器窗口。如果您使用的是 Chromium/Firefox 衍生浏览器,其应用名称可能尚未被识别。', + }, settings: { title: '设置', unsavedCategoriesLeave: '分类有未保存的更改,确定要离开吗?', diff --git a/src/queries.ts b/src/queries.ts index cd30aa320..64ba9fe5b 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -325,6 +325,8 @@ export const browser_appname_regex: Record = { // here (#927, ActivityWatch/activitywatch#1094). The standalone arc key below only covers // setups where Arc was picked explicitly in the settings, which changes the bucket name. // Fork alternatives are $-anchored so names like "archive" / "Dialog" don't match. + // When a standalone Arc bucket is present, browserEvents() drops `arc` from the chrome + // pattern so the same web events are not counted through both buckets. chrome: '(?i)^(google[-_ ]?chrome|chrome|chromium|arc(\\.exe)?$|dia(\\.exe)?$)', firefox: '(?i)(firefox|librewolf|waterfox|nightly)', opera: '(?i)(opera)', @@ -345,13 +347,20 @@ function browserEvents(params: DesktopQueryParams): string { browser_events = []; `; - _.each(browsersWithBuckets(params.bid_browsers), ([browserName, bucketId]) => { + const browsers = browsersWithBuckets(params.bid_browsers); + const hasStandaloneArcBucket = browsers.some(([browserName]) => browserName === 'arc'); + + _.each(browsers, ([browserName, bucketId]) => { const browser_appnames_str = JSON.stringify(browser_appnames[browserName]); code += `events_${browserName} = flood(query_bucket("${bucketId}")); window_${browserName} = filter_keyvals(events, "app", ${browser_appnames_str});`; - // Add regex-based matching to cover case/spacing/versioning variants (e.g., Firefox.exe, firefox-esr-esr140) - const pattern = browser_appname_regex[browserName]; + // Add regex-based matching to cover case/spacing/versioning variants (e.g., Firefox.exe, firefox-esr-esr140). + // A standalone Arc bucket owns Arc events when present; do not also match them through Chrome. + const pattern = + browserName === 'chrome' && hasStandaloneArcBucket + ? '(?i)^(google[-_ ]?chrome|chrome|chromium|dia(\\.exe)?$)' + : browser_appname_regex[browserName]; if (pattern) { code += ` window_${browserName}_re = filter_keyvals_regex(events, "app", ${JSON.stringify(pattern)}); diff --git a/test/unit/queries.test.node.ts b/test/unit/queries.test.node.ts index 2adf8c6bb..5a8b2d0f9 100644 --- a/test/unit/queries.test.node.ts +++ b/test/unit/queries.test.node.ts @@ -261,20 +261,39 @@ describe('browser_appname_regex', () => { }); describe('chrome fork matching in generated query', () => { + const params = { + bid_window: 'aw-watcher-window_testhost', + bid_afk: 'aw-watcher-afk_testhost', + filter_afk: true, + include_audible: false, + categories: [], + filter_categories: [], + }; + test('chrome bucket query includes Dia bundle id and process-name regex', () => { const query = fullDesktopQuery({ - bid_window: 'aw-watcher-window_testhost', - bid_afk: 'aw-watcher-afk_testhost', + ...params, bid_browsers: ['aw-watcher-web-chrome_testhost'], - filter_afk: true, - include_audible: false, - categories: [], - filter_categories: [], }).join('\n'); expect(query).toContain('company.thebrowser.dia'); // JSON.stringify doubles the regex backslash, so the query text has \\. expect(query).toContain('dia(\\\\.exe)?$'); }); + + test('standalone Arc bucket prevents Arc from also matching the chrome bucket', () => { + const query = fullDesktopQuery({ + ...params, + bid_browsers: ['aw-watcher-web-chrome_testhost', 'aw-watcher-web-arc_testhost'], + }).join('\n'); + const chromeWindowFilter = query.slice( + query.indexOf('window_chrome_re ='), + query.indexOf('events_chrome = filter_period_intersect') + ); + expect(chromeWindowFilter).toContain('dia(\\\\.exe)?$'); + expect(chromeWindowFilter).not.toContain('arc(\\\\.exe)?$'); + expect(query).toContain('window_arc_re ='); + expect(query).toContain('arc(\\\\.exe)?$'); + }); }); describe('querystr_to_array', () => { From f6b60a652090bfac06cc0d6ac74abd33c0cc26fd Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 28 Aug 2026 06:37:01 +0000 Subject: [PATCH 3/6] fix(i18n): extend existing activity locale groups Git-Session-Id: 47fd40d1-68e2-5710-b21c-e61a21b8e5cd --- src/i18n/locales/de.ts | 6 ++---- src/i18n/locales/en.ts | 6 ++---- src/i18n/locales/ru.ts | 6 ++---- src/i18n/locales/uk.ts | 6 ++---- src/i18n/locales/zh-CN.ts | 6 ++---- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index bbc01b451..d02fbe404 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -61,10 +61,6 @@ export default { title: 'Hoppla, diese Seite wurde nicht gefunden!', hint: 'Versuchen Sie, dorthin zurückzukehren, woher Sie kamen.', }, - activity: { - browserAllowlistMiss: - 'Für diesen Zeitraum wurde kein passendes Browserfenster gefunden. Falls Sie in einem Chromium-/Firefox-Derivat gesurft haben, wird dessen App-Name möglicherweise noch nicht erkannt.', - }, settings: { title: 'Einstellungen', unsavedCategoriesLeave: 'Ihre Kategorien haben ungespeicherte Änderungen. Wirklich verlassen?', @@ -272,6 +268,8 @@ export default { }, activity: { title: 'Aktivität', + browserAllowlistMiss: + 'Für diesen Zeitraum wurde kein passendes Browserfenster gefunden. Falls Sie in einem Chromium-/Firefox-Derivat gesurft haben, wird dessen App-Name möglicherweise noch nicht erkannt.', for: 'für', host: 'Host:', timeActive: 'Aktive Zeit:', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index ba8d470a9..1b57c6990 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -62,10 +62,6 @@ export default { title: 'Oops, this page was not found!', hint: 'Try navigating back where you came from.', }, - activity: { - browserAllowlistMiss: - 'No matching browser window for this period. If you were browsing in a Chromium/Firefox fork, its app name may not be recognized yet.', - }, settings: { title: 'Settings', unsavedCategoriesLeave: 'Your categories have unsaved changes, are you sure you want to leave?', @@ -273,6 +269,8 @@ export default { }, activity: { title: 'Activity', + browserAllowlistMiss: + 'No matching browser window for this period. If you were browsing in a Chromium/Firefox fork, its app name may not be recognized yet.', for: 'for', host: 'Host:', timeActive: 'Time active:', diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index a7473b4ea..98bf6b6db 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -60,10 +60,6 @@ export default { title: 'Ой, эта страница не найдена!', hint: 'Попробуйте вернуться туда, откуда пришли.', }, - activity: { - browserAllowlistMiss: - 'Для этого периода не найдено подходящее окно браузера. Если вы использовали форк Chromium/Firefox, имя его приложения может пока не распознаваться.', - }, settings: { title: 'Настройки', unsavedCategoriesLeave: 'В категориях есть несохранённые изменения. Действительно выйти?', @@ -269,6 +265,8 @@ export default { }, activity: { title: 'Активность', + browserAllowlistMiss: + 'Для этого периода не найдено подходящее окно браузера. Если вы использовали форк Chromium/Firefox, имя его приложения может пока не распознаваться.', for: 'для', host: 'Хост:', timeActive: 'Активное время:', diff --git a/src/i18n/locales/uk.ts b/src/i18n/locales/uk.ts index 19b780356..59d8d2b28 100644 --- a/src/i18n/locales/uk.ts +++ b/src/i18n/locales/uk.ts @@ -61,10 +61,6 @@ export default { title: 'Ой, цю сторінку не знайдено!', hint: 'Спробуйте повернутися туди, звідки прийшли.', }, - activity: { - browserAllowlistMiss: - 'Для цього періоду не знайдено відповідного вікна браузера. Якщо ви користувалися форком Chromium/Firefox, назва його застосунку може ще не розпізнаватися.', - }, settings: { title: 'Налаштування', unsavedCategoriesLeave: 'У категоріях є незбережені зміни. Справді вийти?', @@ -269,6 +265,8 @@ export default { }, activity: { title: 'Активність', + browserAllowlistMiss: + 'Для цього періоду не знайдено відповідного вікна браузера. Якщо ви користувалися форком Chromium/Firefox, назва його застосунку може ще не розпізнаватися.', for: 'для', host: 'Хост:', timeActive: 'Активний час:', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 8c28d8353..5336274b6 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -61,10 +61,6 @@ export default { title: '页面未找到!', hint: '请尝试返回之前所在的页面。', }, - activity: { - browserAllowlistMiss: - '此时间段内未找到匹配的浏览器窗口。如果您使用的是 Chromium/Firefox 衍生浏览器,其应用名称可能尚未被识别。', - }, settings: { title: '设置', unsavedCategoriesLeave: '分类有未保存的更改,确定要离开吗?', @@ -264,6 +260,8 @@ export default { }, activity: { title: '活动', + browserAllowlistMiss: + '此时间段内未找到匹配的浏览器窗口。如果您使用的是 Chromium/Firefox 衍生浏览器,其应用名称可能尚未被识别。', for: '查看', host: '主机:', timeActive: '活跃时间:', From 419176d901941659eb4f2f2dde100db8cb35a4af Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 28 Aug 2026 07:07:57 +0000 Subject: [PATCH 4/6] fix(queries): preserve Arc events across mixed buckets Git-Session-Id: 47fd40d1-68e2-5710-b21c-e61a21b8e5cd --- src/queries.ts | 16 +++------------- test/unit/queries.test.node.ts | 8 +++++--- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/queries.ts b/src/queries.ts index 64ba9fe5b..b230398b2 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -325,8 +325,6 @@ export const browser_appname_regex: Record = { // here (#927, ActivityWatch/activitywatch#1094). The standalone arc key below only covers // setups where Arc was picked explicitly in the settings, which changes the bucket name. // Fork alternatives are $-anchored so names like "archive" / "Dialog" don't match. - // When a standalone Arc bucket is present, browserEvents() drops `arc` from the chrome - // pattern so the same web events are not counted through both buckets. chrome: '(?i)^(google[-_ ]?chrome|chrome|chromium|arc(\\.exe)?$|dia(\\.exe)?$)', firefox: '(?i)(firefox|librewolf|waterfox|nightly)', opera: '(?i)(opera)', @@ -347,20 +345,13 @@ function browserEvents(params: DesktopQueryParams): string { browser_events = []; `; - const browsers = browsersWithBuckets(params.bid_browsers); - const hasStandaloneArcBucket = browsers.some(([browserName]) => browserName === 'arc'); - - _.each(browsers, ([browserName, bucketId]) => { + _.each(browsersWithBuckets(params.bid_browsers), ([browserName, bucketId]) => { const browser_appnames_str = JSON.stringify(browser_appnames[browserName]); code += `events_${browserName} = flood(query_bucket("${bucketId}")); window_${browserName} = filter_keyvals(events, "app", ${browser_appnames_str});`; // Add regex-based matching to cover case/spacing/versioning variants (e.g., Firefox.exe, firefox-esr-esr140). - // A standalone Arc bucket owns Arc events when present; do not also match them through Chrome. - const pattern = - browserName === 'chrome' && hasStandaloneArcBucket - ? '(?i)^(google[-_ ]?chrome|chrome|chromium|dia(\\.exe)?$)' - : browser_appname_regex[browserName]; + const pattern = browser_appname_regex[browserName]; if (pattern) { code += ` window_${browserName}_re = filter_keyvals_regex(events, "app", ${JSON.stringify(pattern)}); @@ -370,8 +361,7 @@ function browserEvents(params: DesktopQueryParams): string { code += ` events_${browserName} = filter_period_intersect(events_${browserName}, window_${browserName}); events_${browserName} = split_url_events(events_${browserName}); - browser_events = concat(browser_events, events_${browserName}); - browser_events = sort_by_timestamp(browser_events);`; + browser_events = union_no_overlap(browser_events, events_${browserName});`; }); return code; } diff --git a/test/unit/queries.test.node.ts b/test/unit/queries.test.node.ts index 5a8b2d0f9..a4229e49f 100644 --- a/test/unit/queries.test.node.ts +++ b/test/unit/queries.test.node.ts @@ -280,7 +280,7 @@ describe('chrome fork matching in generated query', () => { expect(query).toContain('dia(\\\\.exe)?$'); }); - test('standalone Arc bucket prevents Arc from also matching the chrome bucket', () => { + test('mixed chrome and Arc buckets preserve both matching paths without overlap', () => { const query = fullDesktopQuery({ ...params, bid_browsers: ['aw-watcher-web-chrome_testhost', 'aw-watcher-web-arc_testhost'], @@ -289,10 +289,12 @@ describe('chrome fork matching in generated query', () => { query.indexOf('window_chrome_re ='), query.indexOf('events_chrome = filter_period_intersect') ); - expect(chromeWindowFilter).toContain('dia(\\\\.exe)?$'); - expect(chromeWindowFilter).not.toContain('arc(\\\\.exe)?$'); + expect(chromeWindowFilter).toContain('arc(\\\\.exe)?$'); expect(query).toContain('window_arc_re ='); expect(query).toContain('arc(\\\\.exe)?$'); + expect(query).toContain('browser_events = union_no_overlap(browser_events, events_chrome);'); + expect(query).toContain('browser_events = union_no_overlap(browser_events, events_arc);'); + expect(query).not.toContain('browser_events = concat(browser_events, events_'); }); }); From a2bd9f0df04e2b296a99b055e9fa76abf4ca3784 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 31 Aug 2026 15:15:58 +0000 Subject: [PATCH 5/6] fix(i18n): add Swedish browserAllowlistMiss string Master gained sv.ts via #947 after this PR opened. Keep the empty-state key in every locale so Swedish does not silently fall back to English. Git-Session-Id: 47fd40d1-68e2-5710-b21c-e61a21b8e5cd --- src/i18n/locales/sv.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/locales/sv.ts b/src/i18n/locales/sv.ts index 4af2baf17..a4ef29112 100644 --- a/src/i18n/locales/sv.ts +++ b/src/i18n/locales/sv.ts @@ -273,6 +273,8 @@ export default { }, activity: { title: 'Aktivitet', + browserAllowlistMiss: + 'Inget matchande webbläsarfönster för den här perioden. Om du surfade i en Chromium-/Firefox-fork kanske dess appnamn inte känns igen ännu.', for: 'för', host: 'Värd:', timeActive: 'Aktiv tid:', From 06d1d87d080e4eadcdd159c16fc78f2eb7983b23 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 31 Aug 2026 15:28:53 +0000 Subject: [PATCH 6/6] fix(queries): union only chrome+Arc duplicate streams union_no_overlap across every browser bucket dropped legitimate concurrent activity from later streams (Chrome+Firefox, etc.). Keep concat for distinct browsers; union only the chrome/Arc pair that can actually duplicate the same events. Git-Session-Id: 47fd40d1-68e2-5710-b21c-e61a21b8e5cd --- src/queries.ts | 28 ++++++++++++++++++++++--- test/unit/queries.test.node.ts | 38 +++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/queries.ts b/src/queries.ts index b230398b2..479037ad2 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -341,11 +341,20 @@ export const browser_appname_regex: Record = { // Returns a list of active browser events (where the browser was the active window) from all browser buckets function browserEvents(params: DesktopQueryParams): string { + const browsers = browsersWithBuckets(params.bid_browsers); + // Chrome regex also matches Arc, and a settings-override Arc bucket can + // coexist with the default chrome bucket. Those two streams can duplicate + // the same Arc activity; union_no_overlap is only for that pair. Distinct + // browsers (Chrome + Firefox, etc.) may overlap in time and must concat. + const mixChromeArc = + browsers.some(([browserName]) => browserName === 'chrome') && + browsers.some(([browserName]) => browserName === 'arc'); + let code = ` browser_events = []; `; - _.each(browsersWithBuckets(params.bid_browsers), ([browserName, bucketId]) => { + _.each(browsers, ([browserName, bucketId]) => { const browser_appnames_str = JSON.stringify(browser_appnames[browserName]); code += `events_${browserName} = flood(query_bucket("${bucketId}")); window_${browserName} = filter_keyvals(events, "app", ${browser_appnames_str});`; @@ -358,11 +367,24 @@ function browserEvents(params: DesktopQueryParams): string { window_${browserName} = sort_by_timestamp(concat(window_${browserName}, window_${browserName}_re));`; } + const combineChromeArcDup = mixChromeArc && (browserName === 'chrome' || browserName === 'arc'); code += ` events_${browserName} = filter_period_intersect(events_${browserName}, window_${browserName}); - events_${browserName} = split_url_events(events_${browserName}); - browser_events = union_no_overlap(browser_events, events_${browserName});`; + events_${browserName} = split_url_events(events_${browserName});`; + if (!combineChromeArcDup) { + code += ` + browser_events = concat(browser_events, events_${browserName}); + browser_events = sort_by_timestamp(browser_events);`; + } }); + + if (mixChromeArc) { + // Chrome first so current chrome-bucket events win over a stale Arc bucket. + code += ` + chrome_arc_events = union_no_overlap(events_chrome, events_arc); + browser_events = concat(browser_events, chrome_arc_events); + browser_events = sort_by_timestamp(browser_events);`; + } return code; } diff --git a/test/unit/queries.test.node.ts b/test/unit/queries.test.node.ts index a4229e49f..39b2ceb5e 100644 --- a/test/unit/queries.test.node.ts +++ b/test/unit/queries.test.node.ts @@ -292,9 +292,41 @@ describe('chrome fork matching in generated query', () => { expect(chromeWindowFilter).toContain('arc(\\\\.exe)?$'); expect(query).toContain('window_arc_re ='); expect(query).toContain('arc(\\\\.exe)?$'); - expect(query).toContain('browser_events = union_no_overlap(browser_events, events_chrome);'); - expect(query).toContain('browser_events = union_no_overlap(browser_events, events_arc);'); - expect(query).not.toContain('browser_events = concat(browser_events, events_'); + // Duplicate chrome/Arc streams are unioned with each other, not with every browser. + expect(query).toContain('chrome_arc_events = union_no_overlap(events_chrome, events_arc);'); + expect(query).toContain('browser_events = concat(browser_events, chrome_arc_events);'); + expect(query).not.toContain( + 'browser_events = union_no_overlap(browser_events, events_chrome);' + ); + expect(query).not.toContain('browser_events = union_no_overlap(browser_events, events_arc);'); + }); + + test('unrelated browser buckets concat instead of dropping overlaps', () => { + const query = fullDesktopQuery({ + ...params, + bid_browsers: ['aw-watcher-web-chrome_testhost', 'aw-watcher-web-firefox_testhost'], + }).join('\n'); + expect(query).toContain('browser_events = concat(browser_events, events_chrome);'); + expect(query).toContain('browser_events = concat(browser_events, events_firefox);'); + expect(query).not.toContain('union_no_overlap(browser_events, events_'); + expect(query).not.toContain('union_no_overlap(events_chrome, events_firefox)'); + expect(query).not.toContain('chrome_arc_events'); + }); + + test('chrome+arc union does not swallow a third browser', () => { + const query = fullDesktopQuery({ + ...params, + bid_browsers: [ + 'aw-watcher-web-chrome_testhost', + 'aw-watcher-web-arc_testhost', + 'aw-watcher-web-firefox_testhost', + ], + }).join('\n'); + expect(query).toContain('chrome_arc_events = union_no_overlap(events_chrome, events_arc);'); + expect(query).toContain('browser_events = concat(browser_events, events_firefox);'); + expect(query).toContain('browser_events = concat(browser_events, chrome_arc_events);'); + expect(query).not.toContain('union_no_overlap(browser_events, events_firefox)'); + expect(query).not.toContain('union_no_overlap(browser_events, events_chrome)'); }); });