Skip to content
Merged
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
8 changes: 6 additions & 2 deletions src/util/categorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ export function findCommonPhrases(
const words = new Map<string, WordEntry>();
const bigrams = new Map<string, { bigram: string; duration: number; events: IEvent[] }>();

// Android/ScreenTime events have no title (only an app name), so fall
// back to the app field there. Empty titles count as missing.
const eventText = (event: IEvent): string => event.data.title || event.data.app || '';

// Step 1: Build word duration dictionary
for (const event of events) {
for (const word of event.data.title.split(SPLIT_REGEX)) {
for (const word of eventText(event).split(SPLIT_REGEX)) {
if (word.length <= 2 || ignored_words.includes(word)) {
continue;
}
Expand All @@ -46,7 +50,7 @@ export function findCommonPhrases(

// Step 2: Build bigram duration dictionary (skip bigrams with filtered words)
for (const event of events) {
const parts = event.data.title.split(SPLIT_REGEX);
const parts = eventText(event).split(SPLIT_REGEX);
for (let i = 0; i < parts.length - 1; i++) {
const w1 = parts[i];
const w2 = parts[i + 1];
Expand Down
29 changes: 25 additions & 4 deletions src/views/settings/CategoryBuilder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ div
th Title
th.text-right Duration
tr(v-for="event in showing_events[1]")
td {{ event.data.title }}
td {{ event.data.title || event.data.app }}
td.text-right {{ Math.round(event.duration) }}s
hr
div.d-flex.align-items-center.mt-3(v-if="hasMoreWords")
Expand Down Expand Up @@ -230,11 +230,32 @@ export default {
}
await this.categoryStore.load();
const awclient = getClient();

// Hosts without a window/AFK bucket pair (Android, iOS/ScreenTime import)
// need to be queried through their android-style bucket instead, mirroring
// query_android in the activity store (which also prefers the ScreenTime
// bucket when both exist for a host).
const bucketsStore = useBucketsStore();
const hostname = this.queryOptions.hostname;
const windowAvail =
bucketsStore.bucketsWindow(hostname).length > 0 &&
bucketsStore.bucketsAFK(hostname).length > 0;
const androidBuckets = bucketsStore.bucketsAndroid(hostname);
let bucketParams;
if (!windowAvail && androidBuckets.length > 0) {
const screentimeBucket = androidBuckets.find(id => id.startsWith('aw-import-screentime'));
bucketParams = { bid_android: screentimeBucket || androidBuckets[0] };
} else {
bucketParams = {
bid_window: 'aw-watcher-window_' + hostname,
bid_afk: 'aw-watcher-afk_' + hostname,
filter_afk: this.queryOptions.filter_afk,
};
}

const query =
canonicalEvents({
bid_window: 'aw-watcher-window_' + this.queryOptions.hostname,
bid_afk: 'aw-watcher-afk_' + this.queryOptions.hostname,
filter_afk: this.queryOptions.filter_afk,
...bucketParams,
categories: this.categoryStore.classes_for_query,
filter_categories: [this.category],
}) + 'RETURN = limit_events(sort_by_duration(events), 1000);';
Expand Down
11 changes: 11 additions & 0 deletions test/unit/categorization.test.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ describe('findCommonPhrases', () => {
expect(findCommonPhrases([], [])).toEqual(new Map());
});

test('falls back to app name for events without a title (Android)', () => {
const events: IEvent[] = [
{ timestamp: new Date().toISOString(), duration: 100, data: { app: 'Firefox' } },
{ timestamp: new Date().toISOString(), duration: 70, data: { app: 'Firefox' } },
// An empty title should also fall back to the app name
{ timestamp: new Date().toISOString(), duration: 30, data: { title: '', app: 'Firefox' } },
];
const result = findCommonPhrases(events, []);
expect(result.get('Firefox')?.duration).toBeCloseTo(200);
});

test('counts single words by duration', () => {
// Single-word titles produce no bigrams, so durations accumulate directly
const events = [makeEvent('hello', 100), makeEvent('hello', 50), makeEvent('world', 80)];
Expand Down
Loading