Skip to content

feat(activity): choose all devices or a subset of devices in the Activity view - #1004

Merged
ErikBjare merged 3 commits into
masterfrom
feat/activity-multidevice-selector
Sep 26, 2026
Merged

ErikBjare merged 3 commits into
masterfrom
feat/activity-multidevice-selector

Conversation

@ErikBjare

@ErikBjare ErikBjare commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Motivation

Multidevice was only reachable through the "Use multidevice query" developer setting, which silently turned every /activity/<host> URL into a combined query over all hosts. This makes it a first-class choice in the Activity view: pick All devices, any subset of devices, or a single device, and the choice is part of the URL.

What changed

Device selector. The "Host:" line in the Activity header becomes a dropdown (shown when there is more than one device):

  • All devices
  • a checkbox per device (desktop and mobile icons), to build any subset
  • an "only" link per device, to jump straight to that single device

The nav "Activity" menu gets an All devices entry (only when there are 2+ devices), and the landing-page setting offers "Activity (All devices)".

Route scheme. The selection lives in the existing :host param, so no new routes:

URL Meaning
/activity/<host>/<period>/<date> single device (unchanged)
/activity/host1,host2/<period>/<date> a subset of devices
/activity/@all/<period>/<date> all devices with activity data (resolved at query time, so new devices show up automatically)

@ and , don't occur in DNS hostnames. Bucket hostnames are free-form though (ScreenTime imports produce hostnames like ios-('2E78...', None)), so:

  • a param that exactly matches a known hostname is always that single host, which keeps every existing single-host URL working whatever it contains;
  • inside a list, each hostname is percent-encoded once more, so a comma inside a hostname can't be mistaken for the separator. Plain hostnames ([A-Za-z0-9._-]) are unaffected, so URLs stay readable.

A selection that resolves to one device (e.g. @all on a single-device install) uses the regular single-device view with all its features.

Store/query path (src/stores/activity.ts), when several devices are selected:

  • includes synced hosts (-synced-from-<host> bucket ids, via the existing buildMultideviceHostParams) and Android/ScreenTime hosts;
  • period-usage bars now cover all selected devices (new multideviceActivityQuery: desktop not-afk periods and mobile app usage, period_unioned so simultaneous use counts once), requested in the same bounded spans as the single-device history from feat(activity): always show Year period, remove showYearly setting #1003. Previously they were afk-only, so phones contributed nothing;
  • category-by-period (the timeline barchart) now uses the combined events. It previously always queried the route's single host, even with the setting on;
  • editor buckets of all selected devices are included;
  • ScreenTime hosts show app names rather than bundle IDs, as in the single-device view;
  • per-host query variables include the host index, so hostnames that normalize alike (work-laptop, worklaptop) can't overwrite each other;
  • the cached active history is cleared when the selection changes (it previously survived host switches, see the old FIXME).

Browser and stopwatch data stay single-device only, as before; the header says so when several devices are selected.

useMultidevice setting: removed. The route now expresses the same thing per view. Keeping a global switch would make single-host URLs mean different things depending on a hidden setting, and it was a dev-only "early experiment" toggle. Users who had it enabled can use the new All devices entry. Stale stored values are skipped via REMOVED_KEYS, like showYearly in #1003.

Bug fixed along the way

Android events were pre-merged before the cross-device union. In canonicalEvents, Android watcher events go through merge_events_by_keys(events, ["app"]). That's fine for the single-device Android view, which only computes per-app totals, but in the multidevice query the merged events are then fed to union_no_overlap as a timeline. merge_events_by_keys collapses each app into one event at its first timestamp with the summed duration, and (in aw-server-rust) returns them in hash-map order, while union_no_overlap expects sorted, non-overlapping input. Measured on a real synced phone bucket for one day:

self-overlap placement
pre-merged (before) 5818 s of the 19623 s overlapped 133 min in a single hour; most evening usage (19:00 to 23:00) gone
raw events (this PR) 0 s matches the bucket

The multidevice query now keeps the raw (flooded) events (AndroidQueryParams.keep_event_timestamps); the single-device Android query is unchanged.

Active-history cache (separate commit, pre-existing): the cache was checked with _.includes(history, period), which searches the cached values rather than the period keys, so it never hit and every date step re-queried the whole surrounding range (up to 16 years in Year view). It now checks keys and always re-queries the period containing now. Stepping back one day went from 17 periods per request to 2.

Testing

  • New test/unit/hostSelection.test.node.ts: parse/format round-trips (including comma and % hostnames), resolution order, toggling, eligible hosts.
  • New test/unit/store/activityMultidevice.test.node.ts: selection resolution in the store, synced and Android bucket ids.
  • test/unit/route.test.js: single, @all, list, and escaped-list URLs resolve to the activity view with the expected host param.
  • test/multidevice.test.node.ts: no Android pre-merge in the multidevice query (still present in the single-device one), multideviceActivityQuery.
  • npm test (418 passing), npm run lint, tsc --noEmit, npm run build.
  • Manually against a real aw-server (v0.14.0b8) with 12 devices (local, 7 synced desktops, a synced Android phone, 2 ScreenTime imports): All devices, subset via checkboxes, "only" links, and escaped ScreenTime hostnames in a list all work, and single-host URLs render as before. No screenshots attached since they show personal activity data.

Related: ActivityWatch/activitywatch#302 (how hostnames/devices work), #987, #906.

Rebased on #1001 (which now carries the query-comment fix this PR originally duplicated) and #1003.

@ErikBjare

Copy link
Copy Markdown
Member Author

@greptileai review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T23:27:52.102565Z c90589e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.83117% with 72 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.31%. Comparing base (eef32d5) to head (c90589e).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/stores/activity.ts 51.03% 67 Missing and 4 partials ⚠️
src/util/multidevice.ts 98.43% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1004      +/-   ##
==========================================
+ Coverage   58.78%   61.31%   +2.52%     
==========================================
  Files          51       51              
  Lines        3254     3428     +174     
  Branches      799      841      +42     
==========================================
+ Hits         1913     2102     +189     
+ Misses       1265     1247      -18     
- Partials       76       79       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps

greptile-apps Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Activity view gains multi-device selection UI and routing.

The PR appears safe to merge based on the reviewed changes.

Summary

The PR adds URL-backed selection of all devices or a device subset in Activity, routes combined results into activity and history queries, and replaces the developer-only multidevice setting. The change since the previous review adjusts availability flags for mobile-only selections and adds corresponding tests.

Reviews (6) · Last reviewed commit: "fix(activity): reuse cached active histo..."

Comment thread src/stores/activity.ts Outdated
Comment thread src/views/activity/Activity.vue Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95c950409a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/util/multidevice.ts
@ErikBjare
ErikBjare force-pushed the feat/activity-multidevice-selector branch from 95c9504 to f9a72f6 Compare September 26, 2026 14:33
@ErikBjare

Copy link
Copy Markdown
Member Author

@greptileai review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f9a72f62c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/util/multidevice.ts
@ErikBjare
ErikBjare force-pushed the feat/activity-multidevice-selector branch from f9a72f6 to 2647a2e Compare September 26, 2026 14:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2647a2e9d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/stores/activity.ts
Comment thread src/stores/activity.ts Outdated
Comment thread src/util/multidevice.ts
@TimeToBuildBob

TimeToBuildBob commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

🤖 AI code review

This PR replaces the developer-setting-gated multidevice query with a first-class device selector in the Activity view. It adds a route-based host selection scheme (single host, comma-separated list, or @ALL), a dropdown UI in Activity.vue and Header.vue, and reworks the activity store to query multiple devices with per-host bucket overrides, combined active history, category-by-period, and ScreenTime app-name resolution. It also removes the useMultidevice setting and adds several unit tests for the new host-selection and query logic.

Safe to merge — no P0/P1 findings

Confidence 5/5

✅ No thread-worthy findings. Advisory notes follow; they are retained without opening review threads.

5 advisory findings (summary-only, not scored)

These P2 guard, heuristic, trade-off, or documentation claims are retained for judgment without opening review threads.

⚠️ P2 medium — src/stores/activity.ts:472

In ensure_loaded_multidevice, the availability flags are set based on hasDesktop which checks whether any host has both a window and an afk bucket. However, the actual multidevice query (multidevice_params -> buildMultideviceHostParams) may drop hosts that lack usable buckets, and more importantly, a host with only an android bucket is considered mobile. The flag this.android.available = !hasDesktop is set to true when no desktop host is selected. But the multidevice query for mobile-only hosts still uses the multidevice query path (canonicalMultideviceEvents) which produces window results with app_events and title_events. The Activity view's Android-specific visualizations may rely on android.available to decide how to render. Setting android.available = true for a mobile-only multidevice selection causes the view to behave like the single-device Android view, but the data structure returned by query_multidevice_full is the same as desktop (window.app_events, etc.), not the Android-specific shape. This could lead to visualizations expecting Android data (e.g., with 'app' and 'package' fields) receiving desktop-shaped data. However, the multidevice query's RETURN is the same window object regardless of host types, so the data shape is consistent. The flag is used in the template to decide which visualizations to show; setting android.available true for mobile-only multidevice may show Android-specific visualizations that expect a single android bucket's data, but the multidevice data is aggregated across hosts. This is a potential mismatch but not clearly a bug without knowing the visualization logic.

this.android.available = hosts.some(h => bucketsStore.bucketsAndroid(h).length > 0);

How this was verified: Checked the logic in ensure_loaded_multidevice lines 464-475. The multidevice query includes both desktop and android hosts, and the returned app_events include mobile apps. But the availability flag android.available is set to false whenever hasDesktop is true, so the view will not render Android-specific visualizations even though the data is present. The test at line 124 expects this behavior, but it is a design choice that hides mobile data in combined views.

⚠️ P2 medium — src/util/multidevice.ts:114

In parseHostParam, the logic for a comma-separated list checks items.length > 1 && items.every(h => knownHosts.includes(h)) before checking if the whole param is a known host. This means a URL like /activity/a,b where both 'a' and 'b' are known hosts will be interpreted as a list, even if there is also a host literally named 'a,b'. The comment says formatHostParam links such a single host in escaped form, so it stays reachable. However, the escaped form for a single host 'a,b' is a%252Cb (double-encoded). When the router decodes the path segment once, the param becomes a%2Cb. parseHostParam then sees a param containing a comma (after the first decode? Actually the param is a%2Cb which does not contain a literal comma, so it goes to the knownHosts.includes(param) check? Let's trace: param = 'a%2Cb'. It does not include HOST_SEPARATOR (','), so items = []. Then knownHosts.includes('a%2Cb') is false. Then decoded = _decode('a%2Cb') = 'a,b'. If knownHosts includes 'a,b', it returns that single host. So the escaped form works. But the test 'prefers a list of known hosts over a known host spelled the same' expects parseHostParam('a,b', known) to return the list, and the escaped form to reach the single host. That is consistent. However, there is a subtle issue: if a user manually types a URL with a comma and the host list contains a host that is a substring? Not a bug.

if (knownHosts.includes(param)) {
    return { all: false, hosts: [param] };
  }
  const items = param.includes(HOST_SEPARATOR) ? ... : [];

How this was verified: Checked the order of checks in parseHostParam: lines 103-125. The list check at line 114 comes before the knownHosts.includes(param) check at line 117. The test at line 49-59 explicitly tests the case where 'a,b' is a known host and 'a' and 'b' are also known, expecting the list interpretation. This means existing raw URLs to a comma-containing hostname are broken if the split pieces are also known hosts. The PR description says the opposite.

⚠️ P2 medium — src/stores/activity.ts:710

In query_category_time_by_period, when multideviceParams is used, the query is built with queries.categoryQuery(multideviceParams). The multideviceParams includes hosts and host_params but does not include bid_browsers or bid_stopwatch. The categoryQuery for multidevice uses canonicalMultideviceEvents, which does not include browser or stopwatch events. This is intentional. However, the filter_afk and always_active_pattern are passed through. The multidevice query's canonicalEvents for each host applies the afk filter and always_active_pattern. This is consistent. But there is a potential issue: the multideviceParams is built from this.query_hosts which is set to the resolved hosts. In ensure_loaded_multidevice, this.query_hosts = hosts where hosts is the resolved list. In query_category_time_by_period, it checks this.query_hosts.length > 1 to decide whether to use multideviceParams. If the selection was @ALL but resolved to a single host, ensure_loaded_multidevice is not called (the store uses single-device path), so query_hosts is set to [host] and length is 1, so it uses single-device category query. That is correct. If the selection is an explicit list that resolves to a single host, the store also uses single-device path, so query_hosts is [host]. So the condition works. However, there is a subtle bug: in ensure_loaded_multidevice, this.query_hosts = hosts is set before the queries. If the user changes the selection while a query is in flight, the abort mechanism may cancel, but the query_hosts could be updated by a new ensure_loaded call. The category query uses this.query_hosts at the time it runs, which could be the new selection if the old query wasn't aborted properly. But the abort is called in ensure_loaded when loaded is true. This is a race condition but likely handled by the abort.

How this was verified: Traced the flow: ensure_loaded_multidevice sets query_hosts before querying; category query runs after. Abort on new load should cancel pending requests.

⚠️ P2 medium — src/components/Header.vue:36

In Header.vue, the 'All devices' dropdown item is shown only when activityViews && activityViews.length > 1. However, activityViews is built from buckets grouped by hostname, and it includes hosts with android buckets and non-unknown hosts. If there is only one host with data, the 'All devices' item is hidden, which is correct. But the condition activityViews.length > 1 counts hosts, not devices. If a single host has both a desktop and an android bucket, it appears once in activityViews (as android if android bucket exists, else default). So the count is the number of distinct hostnames. This matches the intent. However, the 'All devices' link uses allDevicesPathUrl which is /activity/@all without a period or date. The Activity route likely defaults to day and today, so it works. But the landing page option uses /activity/@all/view/ which includes the view segment. The Header link does not include /view/, but the route may redirect. This is consistent with existing per-host links which also omit the view. Not a bug.

How this was verified: Checked the activityViews construction and the route definitions (not shown but inferred).

⚠️ P2 medium — src/queries.ts:622

In multideviceActivityQuery, the mobile hosts' app-usage events are unioned into not_afk via period_union(not_afk, query_bucket(...)). However, the query_bucket returns raw events with arbitrary data, and period_union expects events with timestamps and durations. This is fine. But the query does not filter the android events to only those that are 'active' in the sense of app usage; all events in the android bucket are considered active. This matches the single-device Android view which treats all app events as active. However, the android bucket may contain events with zero duration or events that are not app usage (e.g., screen on/off). The single-device activityQueryAndroid also sums all durations, so this is consistent. Not a bug.

How this was verified: Compared with activityQueryAndroid which also sums all events.

Files changed (19) — the diff as I read it
  • src/components/Header.vue — Adds an 'All devices' dropdown item and uses formatHostParam for per-host activity links.
  • src/i18n/locales/de.ts — Removes multidevice setting strings and adds device-selector and multidevice-note strings.
  • src/i18n/locales/en.ts — Removes multidevice setting strings and adds device-selector and multidevice-note strings.
  • src/i18n/locales/ru.ts — Removes multidevice setting strings and adds device-selector and multidevice-note strings.
  • src/i18n/locales/sv.ts — Removes multidevice setting strings and adds device-selector and multidevice-note strings.
  • src/i18n/locales/uk.ts — Removes multidevice setting strings and adds device-selector and multidevice-note strings.
  • src/i18n/locales/zh-CN.ts — Removes multidevice setting strings and adds device-selector and multidevice-note strings.
  • src/queries.ts — Adds keep_event_timestamps to Android params, per-host index suffixes, screentimeNamesQuery, multideviceActivityQuery, and exports them.
  • src/stores/activity.ts — Adds multidevice host resolution, combined querying, active-history caching keyed by selection, and ScreenTime name mapping.
  • src/stores/settings.ts — Removes the useMultidevice setting and adds it to REMOVED_KEYS.
  • src/util/multidevice.ts — Adds host-selection parsing/formatting, eligibility filtering, and toggle logic.
  • src/views/activity/Activity.vue — Adds a device-selector dropdown, host-label computed, and route-building helpers.
  • src/views/settings/DeveloperSettings.vue — Removes the 'Use multidevice query' checkbox.
  • src/views/settings/LandingPageSettings.vue — Adds an 'All devices' landing-page option and uses formatHostParam for host links.
  • test/multidevice.test.node.ts — Adds tests for android event non-pre-merge, multideviceActivityQuery, per-host variable suffixes, and ScreenTime names.
  • test/unit/hostSelection.test.node.ts — Adds tests for parseHostParam, formatHostParam, resolveHostSelection, toggleHostInSelection, and eligibleMultideviceHosts.
  • test/unit/route.test.js — Adds route-resolution tests for the new host param forms.
  • test/unit/store/activityMultidevice.test.node.ts — Adds store tests for host resolution, multidevice params, history cache invalidation, and availability flags.
  • test/unit/store/settings.locale.test.js — Extends the removed-keys test to cover useMultidevice.
Previous review passes
commit score findings engine when
2647a2e9d18f 5/5 0 llm 2026-09-26 15:03 UTC
c646ba33fa30 3/5 2 llm 2026-09-26 23:20 UTC

Reviewed c90589e6dc07 · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 492s · about this reviewer

Maintainer commands

@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.

Don't pre-merge Android watcher events by app in the multidevice query.
merge_events_by_keys collapses each app into one event at its first
timestamp with the summed duration, in arbitrary order, so the per-device
timeline passed to union_no_overlap was fabricated: events overlapped each
other and landed in the wrong hours. The single-device Android view keeps
the pre-merge, since it only computes per-app totals.
@ErikBjare
ErikBjare force-pushed the feat/activity-multidevice-selector branch from 2647a2e to 1936bc9 Compare September 26, 2026 22:44
@ErikBjare

Copy link
Copy Markdown
Member Author

@greptileai review

Comment thread src/util/multidevice.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1936bc98c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/stores/activity.ts Outdated
Comment thread src/stores/activity.ts
@ErikBjare
ErikBjare force-pushed the feat/activity-multidevice-selector branch from 1936bc9 to a70789d Compare September 26, 2026 22:52
@ErikBjare

Copy link
Copy Markdown
Member Author

@greptileai review

Comment thread src/stores/activity.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a70789d167

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/stores/activity.ts Outdated
@ErikBjare
ErikBjare force-pushed the feat/activity-multidevice-selector branch from a70789d to c646ba3 Compare September 26, 2026 23:04
@ErikBjare

Copy link
Copy Markdown
Member Author

@greptileai review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c646ba33fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/stores/activity.ts Outdated
Comment thread src/stores/activity.ts
Comment thread src/stores/activity.ts
…vity view

Multidevice was hidden behind the "Use multidevice query" developer setting,
which silently switched every Activity URL to a combined query over all
hosts. Make it a first-class choice instead: the Activity view gets a device
selector (All devices, or any subset via checkboxes, plus "only" to jump to
a single device), and the Activity nav menu gets an "All devices" entry.

The selection lives in the existing :host route param:

  /activity/<host>/...          single device (unchanged)
  /activity/host1,host2/...     a subset of devices
  /activity/@all/...            all devices with activity data

'@' and ',' don't occur in DNS hostnames. A param that exactly matches a
known hostname is always that single host, and list items are escaped, so
free-form bucket hostnames (e.g. ScreenTime imports, which contain commas)
work both alone and in a list.

In multidevice mode the store now also:
- includes synced (-synced-from-) and Android/ScreenTime hosts;
- computes the period-usage bars from all selected devices (new
  multideviceActivityQuery; simultaneous use counts once), with the same
  bounded request span as the single-device active history (#1003);
- computes category-by-period (timeline barchart) from the combined events,
  which previously always used the route's single host;
- includes editor buckets of all selected devices;
- clears the cached active history when the selection changes.

Browser and stopwatch data remain single-device only, as before; the view
says so when several devices are selected.

The useMultidevice setting and its developer toggle are removed: the route
now expresses the same thing per view, and keeping a global switch would
make single-host URLs ambiguous. Stale stored values are skipped via
REMOVED_KEYS, like showYearly in #1003.
… period

The period-usage history cache was checked with
_.includes(history, period), which searches the cached event lists rather
than the period keys, so it never hit and every navigation re-queried the
whole surrounding range (up to 16 years in Year view). Check the keys, and
keep re-querying the period that contains now, since it's still growing.
Applies to the single-device, Android and multidevice history queries.
@ErikBjare
ErikBjare force-pushed the feat/activity-multidevice-selector branch from c646ba3 to c90589e Compare September 26, 2026 23:24
@ErikBjare

Copy link
Copy Markdown
Member Author

@greptileai review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c90589e6dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/util/multidevice.ts
@ErikBjare
ErikBjare merged commit ddf567f into master Sep 26, 2026
9 checks passed
ErikBjare added a commit that referenced this pull request Sep 26, 2026
useMultidevice is gone (#1004): multi-device views are now selected by
the route (@ALL or a host list) and loaded by ensure_loaded_multidevice.

- ensure_loaded_multidevice follows what the single-device path does for
  custom ranges and All time: no period-usage history when it's skipped,
  the monthly barchart derived from the query chunks for long ranges
  (and no years of active events kept in state), and progress cleared.
- get_earliest_date covers the devices the query will include.
- The single-device monthly-barchart shortcut no longer checks
  useMultidevice.
ErikBjare added a commit that referenced this pull request Sep 27, 2026
useMultidevice is gone (#1004): multi-device views are now selected by
the route (@ALL or a host list) and loaded by ensure_loaded_multidevice.

- ensure_loaded_multidevice follows what the single-device path does for
  custom ranges and All time: no period-usage history when it's skipped,
  the monthly barchart derived from the query chunks for long ranges
  (and no years of active events kept in state), and progress cleared.
- get_earliest_date covers the devices the query will include.
- The single-device monthly-barchart shortcut no longer checks
  useMultidevice.
- Device-selector links keep the route's date part, so switching devices
  keeps a custom range (and doesn't add a date to All time).
ErikBjare added a commit that referenced this pull request Sep 28, 2026
* feat(activity): add custom date range period

Adds a 'custom range' period to the Activity view, encoded in the URL as
/activity/:host/range/YYYY-MM-DD..YYYY-MM-DD (both ends inclusive), so
ranges are shareable and existing URLs keep working.

- Start/end native date inputs replace the single date input in range mode
- Prev/next step by the range length; next is disabled once it would start
  after today (this also fixes the next button never being disabled, since
  `today` was never set)
- Entering range mode from another period keeps the shown period, clipped
  to today
- Ranges longer than 92 days are bucketed by calendar month in the
  timeline barchart and the category-by-period query (shared via
  timeperiodsForBarchart); multi-day barchart labels now show dates

Part of ActivityWatch/activitywatch#1465

* perf(activity): chunk long ranges by week and derive monthly barchart

For ranges long enough to use monthly barchart buckets, split the desktop
query into chunks of at most 7 days that never cross a calendar month, and
build the monthly category data from those chunk results instead of issuing
a month-sized category query per month (10-38s each on a 1.7 GB database,
occasionally past the 30s request timeout).

* perf(activity): keep per-day requests for long ranges

Week-sized chunks were no faster on a 1.7 GB aw-server database and single
requests reached 20s, too close to the 30s timeout. Days never cross a month
boundary either, so the monthly barchart is still derived from them.

* fix(activity): address review on custom ranges

- Skip the period-usage history in range mode and hide its bars: 31
  neighbouring ranges can span decades of AFK data
- Cap the range end at today
- Hide prev/next when the URL range is invalid (they produced invalid links)
- Use the app locale for barchart date/month labels
- Remove a test file that belongs to the All time PR

* fix(activity): clip next range at today

* feat(activity): add All time period (#1006)

* feat(activity): add All time period

- New 'all' period (/activity/:host/all/view/...) from the host's first day
  with data to today, marked with 🐌 plus a slowness hint and a progress bar
- Earliest event: metadata.start on aw-server-rust, otherwise a ~15-request
  bisection with GET /events?end=&limit=1 on aw-server (Python); cached per host
- Skips the period-usage history (no neighbouring periods)
- Editor and Android/ScreenTime queries are split into bounded chunks and
  merged client-side instead of one request for the whole span
- Long ranges don't keep years of AFK events in reactive state

Part of ActivityWatch/activitywatch#1465

* fix(activity): address review on All time

- Earliest date covers every bucket type the view queries (editor, browser,
  stopwatch too) and all hosts when multidevice is on; cache keyed by hosts
  and day-start offset
- Bisection returns a conservative bound (never after the first event)
- Earliest-event lookup failure falls back to bucket creation dates instead
  of blocking the page
- Chunked editor/Android queries over-fetch (1000 per chunk) so items
  outside each chunk's top 100 can still reach the overall top 100
- A failed Android chunk shows no data instead of partial totals
- Progress bar counts editor and Android chunks

* refactor(activity): use the #1004 host selection for ranges and All time

useMultidevice is gone (#1004): multi-device views are now selected by
the route (@ALL or a host list) and loaded by ensure_loaded_multidevice.

- ensure_loaded_multidevice follows what the single-device path does for
  custom ranges and All time: no period-usage history when it's skipped,
  the monthly barchart derived from the query chunks for long ranges
  (and no years of active events kept in state), and progress cleared.
- get_earliest_date covers the devices the query will include.
- The single-device monthly-barchart shortcut no longer checks
  useMultidevice.
- Device-selector links keep the route's date part, so switching devices
  keeps a custom range (and doesn't add a date to All time).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants