Skip to content

Commit 83dc881

Browse files
feat: add work time report view (#775)
* feat: add work time report view Add a new Work Report view that provides daily work time breakdowns with multi-device support, category filtering, configurable break time (gap merging via flood), and CSV/JSON export. Based on #742 by @ErikBjare. Changes from original: - Implemented thisWeek and thisMonth date ranges - Removed debug console.log statements - Used safeHost consistently in find_bucket queries Closes #742 * fix(WorkReport): use original hostname in find_bucket, safeHost only for variable names Bucket IDs preserve the original hostname (e.g. 'aw-watcher-window_my-laptop'), but variable names in the query language must be alphanumeric. The sanitized safeHost is correct for variable naming, but find_bucket must use the original hostname to locate the bucket. * fix(WorkReport): fix startOfDay offset and add AFK filtering Two P1 bugs flagged by Greptile: 1. startOfDay offset was silently ignored — moment().add('04:00') is a no-op (moment expects (amount, unit) or ISO 8601). Now uses get_day_start_with_offset/get_day_end_with_offset from util/time, the same helpers used by every other view. 2. AFK time was counted as work time — the per-host query never intersected with aw-watcher-afk data. Now applies the standard not_afk pattern (flood → filter_keyvals status=not-afk → filter_period_intersect) before categorizing events, matching the canonicalEvents pattern in src/queries.ts. * fix(WorkReport): use query_bucket directly and safeHostname from queries.ts * fix(WorkReport): add empty category guard and fix hostname collision - Add validation: alert when no categories selected (previously silent all-zeros) - Use indexed AQL variables (events_0, events_1, ...) instead of safeHostname() to prevent collisions when hostnames differ only in non-alphanumeric chars (e.g., 'my-laptop' vs 'mylaptop') - Remove unused safeHostname import Addresses Greptile review findings on PR #775. * fix(work-report): guard hosts without AFK buckets * test(router): cover work report route and landingpage redirect * fix(work-report): skip hosts without AFK buckets
1 parent c92b8f9 commit 83dc881

7 files changed

Lines changed: 511 additions & 1 deletion

File tree

src/components/Header.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ div(:class="{'fixed-top-padding': fixedTopMenu}")
5959
b-dropdown-item(to="/search")
6060
icon(name="search")
6161
| Search
62+
b-dropdown-item(to="/work-report")
63+
icon(name="briefcase")
64+
| Work Report
6265
b-dropdown-item(to="/trends" v-if="devmode")
6366
icon(name="chart-line")
6467
| Trends
@@ -98,6 +101,7 @@ div(:class="{'fixed-top-padding': fixedTopMenu}")
98101
<script lang="ts">
99102
// only import the icons you use to reduce bundle size
100103
import 'vue-awesome/icons/calendar-day';
104+
import 'vue-awesome/icons/briefcase';
101105
import 'vue-awesome/icons/calendar-week';
102106
import 'vue-awesome/icons/stream';
103107
import 'vue-awesome/icons/database';

src/queries.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function escape_doublequote(s: string) {
1515
}
1616

1717
// Hostname safe for using as a variable name
18-
function safeHostname(hostname: string): string {
18+
export function safeHostname(hostname: string): string {
1919
return hostname.replace(/[^a-zA-Z0-9_]/g, '');
2020
}
2121

src/route.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const Trends = () => import('./views/Trends.vue');
1515
const Settings = () => import('./views/settings/Settings.vue');
1616
const CategoryBuilder = () => import('./views/settings/CategoryBuilder.vue');
1717
const Stopwatch = () => import('./views/Stopwatch.vue');
18+
const WorkReport = () => import('./views/WorkReport.vue');
1819
const Alerts = () => import('./views/Alerts.vue');
1920
const Search = () => import('./views/Search.vue');
2021
const Report = () => import('./views/Report.vue');
@@ -66,6 +67,7 @@ const router = new VueRouter({
6667
{ path: '/settings', component: Settings },
6768
{ path: '/settings/category-builder', component: CategoryBuilder },
6869
{ path: '/stopwatch', component: Stopwatch },
70+
{ path: '/work-report', component: WorkReport },
6971
{ path: '/search', component: Search },
7072
{ path: '/graph', component: Graph },
7173
{ path: '/dev', component: Dev },

src/util/workReport.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { IBucket } from '~/util/interfaces';
2+
3+
export interface WorkReportHostOption {
4+
value: string;
5+
text: string;
6+
disabled: boolean;
7+
}
8+
9+
function getWindowHosts(buckets: IBucket[]): string[] {
10+
const hosts = buckets
11+
.filter(bucket => bucket.type === 'currentwindow')
12+
.map(bucket => bucket.id.replace('aw-watcher-window_', ''));
13+
return [...new Set(hosts)];
14+
}
15+
16+
function getAFKHosts(buckets: IBucket[]): Set<string> {
17+
return new Set(
18+
buckets
19+
.filter(bucket => bucket.type === 'afkstatus')
20+
.map(bucket => bucket.id.replace('aw-watcher-afk_', ''))
21+
);
22+
}
23+
24+
export function getWorkReportHostOptions(buckets: IBucket[]): WorkReportHostOption[] {
25+
const afkHosts = getAFKHosts(buckets);
26+
return getWindowHosts(buckets).map(host => {
27+
const hasAFK = afkHosts.has(host);
28+
return {
29+
value: host,
30+
text: hasAFK ? host : `${host} (requires aw-watcher-afk)`,
31+
disabled: !hasAFK,
32+
};
33+
});
34+
}
35+
36+
export function getUnsupportedWorkReportHosts(
37+
selectedHosts: string[],
38+
buckets: IBucket[]
39+
): string[] {
40+
const afkHosts = getAFKHosts(buckets);
41+
return selectedHosts.filter(host => !afkHosts.has(host));
42+
}
43+
44+
export function getSupportedWorkReportHosts(selectedHosts: string[], buckets: IBucket[]): string[] {
45+
const unsupportedHosts = new Set(getUnsupportedWorkReportHosts(selectedHosts, buckets));
46+
return selectedHosts.filter(host => !unsupportedHosts.has(host));
47+
}

0 commit comments

Comments
 (0)