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/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) 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 }); + }, + }, }, };