From 701e6df3d3c9fc5038bbfbc7e1c69a115b0e1dd0 Mon Sep 17 00:00:00 2001 From: Bmans Date: Wed, 17 Jun 2026 10:12:41 -0400 Subject: [PATCH 1/2] fix(import): show success message and propagate server error to UI Previously the import UI showed nothing on success (the null response was silent) and displayed only a generic fallback message on failure regardless of what the server reported. - Add a dismissible success alert shown after a successful import - Extract the server error message from the response body and display it; fall back to the generic message if no structured error is present - Add import_success flag to data() to drive the new success alert Fixes https://github.com/ActivityWatch/activitywatch/issues/394 Co-Authored-By: Claude Sonnet 4.6 --- src/views/Buckets.vue | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/views/Buckets.vue b/src/views/Buckets.vue index 1c9100ef..d402554f 100644 --- a/src/views/Buckets.vue +++ b/src/views/Buckets.vue @@ -88,7 +88,9 @@ div b-card-group.deck b-card(header="Import buckets") - b-alert(v-if="import_error" show variant="danger" dismissable) + b-alert(v-if="import_success" show variant="success" dismissible @dismissed="import_success = false") + | Import completed successfully! + b-alert(v-if="import_error" show variant="danger" dismissible @dismissed="import_error = null") | {{ import_error }} b-form-file(v-model="import_file" placeholder="Choose or drop a file here..." @@ -174,6 +176,7 @@ export default { import_file: null, import_error: null, + import_success: false, delete_bucket_selected: null, fields: [ { key: 'id', label: 'Bucket ID', sortable: true }, @@ -191,10 +194,12 @@ export default { await this.importBuckets(this.import_file); console.log('Import successful'); this.import_error = null; + this.import_success = true; } catch (err) { console.log('Import failed'); - // TODO: Make aw-server report error message so it can be shown in the web-ui - this.import_error = 'Import failed, see aw-server logs for more info'; + const serverMessage = err?.response?.data?.message; + this.import_error = serverMessage || 'Import failed, see aw-server logs for more info'; + this.import_success = false; } // We need to reload buckets even if we fail because imports can be partial // (first bucket succeeds, second fails for example when importing multiple) From 637abe95da1f1b31fe5efe21db4a59da6664be54 Mon Sep 17 00:00:00 2001 From: Bmans Date: Wed, 17 Jun 2026 13:04:10 -0400 Subject: [PATCH 2/2] fix(query): make flood pulsetime configurable to fix missing time with high poll intervals Users who raise the watcher polling interval above 5s lose trackable time because the default 5s pulsetime in flood() no longer fills the inter-poll gaps. A user working 8h could see only 7.5h in the dashboard. Changes: - queries.ts: add optional floodPulsetime to BaseQueryParams (default 5); pass it as the explicit second argument to all flood() calls in canonicalEvents() and browserEvents() - settings.ts: add floodPulsetime: number (default 5, persisted like other settings) - stores/activity.ts: thread useSettingsStore().floodPulsetime into both fullDesktopQuery and multideviceQuery param objects - DeveloperSettings.vue: add a number input for floodPulsetime with guidance text explaining the poll_interval + 1 rule The default of 5 is fully backward-compatible. Users who increased their polling interval can now set this to polling_interval + 1 in Developer Settings and immediately recover the missing time. Pairs with aw-core fix: https://github.com/ActivityWatch/aw-core/pull/139 Fixes https://github.com/ActivityWatch/activitywatch/issues/1177 Co-Authored-By: Claude Sonnet 4.6 --- src/queries.ts | 17 ++++++++++++++--- src/stores/activity.ts | 2 ++ src/stores/settings.ts | 7 +++++++ src/views/settings/DeveloperSettings.vue | 12 ++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/queries.ts b/src/queries.ts index 406a3431..c73997de 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -33,6 +33,14 @@ interface BaseQueryParams { bid_browsers?: string[]; bid_stopwatch?: string; return_variable_suffix?: string; + /** + * Seconds used as the pulsetime argument to flood(). + * Must be >= your watcher's polling interval to avoid gaps in the timeline. + * Default: 5 (matches the default 1s poll interval with headroom). + * Increase this if you use a higher polling interval (e.g. set to 31 for 30s polling). + * See: https://github.com/ActivityWatch/activitywatch/issues/1177 + */ + floodPulsetime?: number; } interface DesktopQueryParams extends BaseQueryParams { @@ -125,14 +133,16 @@ export function canonicalEvents(params: DesktopQueryParams | AndroidQueryParams) // For simplicity, we assume that bid_window and bid_android are exchangeable (note however it needs special treatment) const bid_window = isDesktopParams(params) ? params.bid_window : params.bid_android; + const pulsetime = params.floodPulsetime ?? 5; + return [ // Fetch window/app events - `events = flood(${queryBucket(bid_window)});`, + `events = flood(${queryBucket(bid_window)}, ${pulsetime});`, // On Android, merge events to avoid overload of events isAndroidParams(params) ? 'events = merge_events_by_keys(events, ["app"]);' : '', // Fetch not-afk events isDesktopParams(params) - ? `not_afk = flood(${queryBucket(params.bid_afk)}); + ? `not_afk = flood(${queryBucket(params.bid_afk)}, ${pulsetime}); not_afk = filter_keyvals(not_afk, "status", ["not-afk"]);` + (always_active_pattern_str ? `not_treat_as_afk = filter_keyvals_regex(events, "app", "${always_active_pattern_str}"); @@ -279,13 +289,14 @@ 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 pulsetime = params.floodPulsetime ?? 5; let code = ` browser_events = []; `; _.each(browsersWithBuckets(params.bid_browsers), ([browserName, bucketId]) => { const browser_appnames_str = JSON.stringify(browser_appnames[browserName]); - code += `events_${browserName} = flood(query_bucket("${bucketId}")); + code += `events_${browserName} = flood(query_bucket("${bucketId}"), ${pulsetime}); 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) diff --git a/src/stores/activity.ts b/src/stores/activity.ts index 6a277467..2f194f3c 100644 --- a/src/stores/activity.ts +++ b/src/stores/activity.ts @@ -342,6 +342,7 @@ export const useActivityStore = defineStore('activity', { filter_categories, host_params: {}, always_active_pattern, + floodPulsetime: useSettingsStore().floodPulsetime, }); const data = await getClient().query(periods, q, { name: 'multidevice', verbose: true }); this.query_window_completed(data[0].window); @@ -371,6 +372,7 @@ export const useActivityStore = defineStore('activity', { filter_categories, include_audible, always_active_pattern, + floodPulsetime: useSettingsStore().floodPulsetime, }); const data = await getClient().query(periods, q, { name: 'fullDesktopQuery', diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 39fb78cc..350e83ee 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -49,6 +49,12 @@ interface State { showYearly: boolean; useMultidevice: boolean; requestTimeout: number; + /** + * Seconds used as the pulsetime argument to flood() in queries. + * Increase if you use a high watcher polling interval (e.g. set to 31 for 30s polling). + * See: https://github.com/ActivityWatch/activitywatch/issues/1177 + */ + floodPulsetime: number; // Set to true if settings loaded _loaded: boolean; @@ -90,6 +96,7 @@ export const useSettingsStore = defineStore('settings', { showYearly: false, useMultidevice: false, requestTimeout: 30, + floodPulsetime: 5, _loaded: false, }), diff --git a/src/views/settings/DeveloperSettings.vue b/src/views/settings/DeveloperSettings.vue index 95460bfd..7a851fe7 100644 --- a/src/views/settings/DeveloperSettings.vue +++ b/src/views/settings/DeveloperSettings.vue @@ -19,6 +19,10 @@ div div b-input.float-right.ml-2(v-model="requestTimeout" type="number") + b-form-group(label="Flood pulsetime (seconds)" label-cols-md=3 description="Controls how large a gap between events (in seconds) is filled when computing activity durations. The default of 5 works for the standard 1s polling interval. If you have raised the watcher polling interval (e.g. to 30s), set this to polling_interval + 1 to avoid missing time in reports. See issue #1177.") + div + b-input.float-right.ml-2(v-model.number="floodPulsetime" type="number" min="1" step="1") + div | Web UI commit hash: {{ COMMIT_HASH }} @@ -65,6 +69,14 @@ export default { useSettingsStore().update({ requestTimeout }); }, }, + floodPulsetime: { + get() { + return useSettingsStore().floodPulsetime; + }, + set(floodPulsetime) { + useSettingsStore().update({ floodPulsetime }); + }, + }, }, };