Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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});`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Extra argument may break flood() on older aw-server versions

The PR description notes this change pairs with aw-core#139, which adds pulsetime as a named parameter to flood(). Users running an aw-server version that predates that change will suddenly receive a query with an unexpected second positional argument. Depending on how the query language dispatcher handles extra args, this could result in a TypeError or similar runtime error that causes all activity queries to fail — with no fallback.

Before landing, it is worth confirming that the aw-core query interpreter silently ignores extra positional arguments when flood() is called with the old signature, and documenting the minimum required aw-server version if not.

// 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}");
Expand Down Expand Up @@ -279,13 +289,14 @@ export const browser_appname_regex: Record<string, string> = {

// 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)
Expand Down
2 changes: 2 additions & 0 deletions src/stores/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions src/stores/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,6 +96,7 @@ export const useSettingsStore = defineStore('settings', {
showYearly: false,
useMultidevice: false,
requestTimeout: 30,
floodPulsetime: 5,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No store-level bounds validation for floodPulsetime

The HTML input enforces min="1", but the store's update() action accepts any value passed to it directly (e.g. from tests, direct API calls, or a malformed server response). A value of 0 or a negative number would silently produce flood(query_bucket("..."), 0) in the generated query string, which likely has undefined behaviour in the query language. Adding a clamp or guard (e.g. Math.max(1, value)) inside the setter or the update action would be more defensive.


_loaded: false,
}),
Expand Down
11 changes: 8 additions & 3 deletions src/views/Buckets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down Expand Up @@ -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 },
Expand All @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions src/views/settings/DeveloperSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
</template>
Expand Down Expand Up @@ -65,6 +69,14 @@ export default {
useSettingsStore().update({ requestTimeout });
},
},
floodPulsetime: {
get() {
return useSettingsStore().floodPulsetime;
},
set(floodPulsetime) {
useSettingsStore().update({ floodPulsetime });
},
},
},
};
</script>