Skip to content

Commit 1220e12

Browse files
committed
feat: update stats store to ignore errors in fetch methods
feat: add new fields to WidgetRegistryItem interface for enhanced widget capabilities feat: implement local API call capability in ExternalWidgetHost for widgets fix: handle errors gracefully in WidgetWindow during resizing feat: add timelens.apiToken configuration option to VS Code extension docs: update localization files with new apiToken description feat: attach API token in timelensApi for local API calls docs: create Widget SDK v2 migration guide for developers feat: add third-party widget template with local API call example chore: add package.json and tsconfig.json for third-party widget template feat: implement database encryption in Tauri backend feat: create Widget Dev Harness page for testing third-party widgets chore: add Vite environment types for better TypeScript support
1 parent e12b034 commit 1220e12

50 files changed

Lines changed: 5160 additions & 644 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1717
- **Cross-platform legacy 1.x migration** — startup now detects legacy database paths on Windows, macOS, and Linux and migrates them into the default profile.
1818
- **Restore monitoring active state on startup** — monitor no longer defaults to active when the user had paused tracking before quitting.
1919
- **Backup & Restore v2 completeness** — backup packages now include app categories, usage goals, focus sessions/rules, browser ignored domains/limits, widget permissions/audit log, VS Code sessions, API tokens, and client allowlist.
20+
- **Automatic archive scheduler** — added background scheduler with configurable daily run hour and battery-aware toggle, plus `get_archive_scheduler_settings` / `set_archive_scheduler_settings` commands.
21+
- **Hot/warm/archive tiering** — archived rows are now tagged with `tier` (`warm` or `archive`) based on age relative to the retention cutoff.
22+
- **Historical compression** — old archived raw usage rows can be compressed into zstd blobs in `app_usage_archive_compressed`, freeing storage while preserving queryability.
23+
- **Restore diff summary** — backup validation now reports per-table add/update/conflict counts and settings conflicts before applying a restore.
24+
- **`new_profile` restore strategy** — importing a backup can create and switch to a fresh profile instead of overwriting or merging the current one.
25+
- **Passphrase-protected backups** — Backup v2 packages can be AES-256-GCM encrypted with a passphrase derived key (Argon2id).
26+
- **Database encryption at rest** — optional file-level AES-256-GCM encryption for the local SQLite database; runtime plaintext is wiped on app exit so only the encrypted file remains at rest.
27+
- **Database encryption commands** — added `enable_database_encryption`, `disable_database_encryption`, and `get_database_encryption_status`.
28+
- **Derived metrics tables** — added incremental maintenance of `app_switch_density`, `focus_streaks`, and `interruption_summary` in the monitor task and a periodic scheduler, plus `rebuild_derived_metrics` for full repair.
29+
- **Distraction hotspot detection** — added `get_distraction_hotspots` command and data model for ranking high-switch-density / high-fragmentation apps and time windows.
30+
- **Category and project comparison** — added `get_category_comparison_in_ranges` and `get_project_comparison_in_ranges` for period-over-period drill-down beyond apps.
31+
- **Goal risk notifications** — added `evaluate_goal_risks` and a background notifier that emits `goal-risk-alert` events and native notifications when goals are off track.
32+
- **Backend Focus rule automation** — moved Focus rules from frontend-only `localStorage` to the `focus_rules` table with CRUD commands and a background evaluator that auto starts/stops focus sessions.
2033

2134
### Changed
2235

docs/WIDGET_SDK_v2_MIGRATION.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Widget SDK v2 Migration Guide
2+
3+
TimeLens 2.0.0 introduces Widget SDK v2. This guide explains the manifest version policy, capability model, and how to upgrade an existing v1 widget.
4+
5+
## Manifest version policy
6+
7+
- `manifest_version` is now a required top-level integer field.
8+
- Supported values: `1` (legacy, auto-upgraded) and `2` (current).
9+
- If `manifest_version > 2`, TimeLens rejects the widget with the error **"unsupported manifest version"**.
10+
- v1 manifests continue to load, but they are normalized to v2 internally.
11+
12+
## Mapping legacy permissions to capabilities
13+
14+
In v1 you declared `permissions` as method-level strings. In v2 you declare `capabilities`, which are broader security buckets. TimeLens maps between them automatically.
15+
16+
| Legacy permission | v2 capability |
17+
|---|---|
18+
| `screen-time:read` | `read_metrics` |
19+
| `todo:read` | `read_metrics` |
20+
| `todo:write` | `write_data` |
21+
| `settings:write` | `write_data` |
22+
| `active-window:subscribe` | `automation_trigger` |
23+
| `local-api:call` | `local_api_call` |
24+
25+
### Reverse mapping (capability → runtime permissions)
26+
27+
When you declare a capability, TimeLens grants the underlying runtime permission strings so the widget channel works out of the box.
28+
29+
| v2 capability | Runtime permissions granted |
30+
|---|---|
31+
| `read_metrics` | `screen-time:read`, `todo:read` |
32+
| `write_data` | `todo:write`, `settings:write` |
33+
| `automation_trigger` | `active-window:subscribe` |
34+
| `local_api_call` | `local-api:call` |
35+
36+
## New `local-api:call` capability
37+
38+
Widgets can now call the TimeLens local HTTP API directly through the widget channel:
39+
40+
```js
41+
const result = await context.channel.localApiCall({
42+
method: "GET",
43+
path: "/api/screen-time/today",
44+
scopes: ["screen-time:read"],
45+
});
46+
```
47+
48+
Requirements:
49+
50+
1. The widget manifest includes the `local_api_call` capability.
51+
2. The user has granted the `local-api:call` permission to this widget instance.
52+
3. The requested `scopes` match the endpoint being called (e.g. `screen-time:read` for `GET /api/screen-time/today`).
53+
54+
The host automatically issues a scoped local API token and attaches it as the `X-Api-Token` header. The client ID is set to `widget-<widget_id>`, which the local API allowlist treats as a widget identity.
55+
56+
## How to upgrade a v1 widget
57+
58+
1. Add `"manifest_version": 2` to `manifest.json`.
59+
2. Replace or supplement `permissions` with `capabilities`.
60+
61+
Before:
62+
63+
```json
64+
{
65+
"widget_type": "my_widget",
66+
"name": "My Widget",
67+
"entry": "index.js",
68+
"permissions": ["screen-time:read", "active-window:subscribe"]
69+
}
70+
```
71+
72+
After:
73+
74+
```json
75+
{
76+
"manifest_version": 2,
77+
"widget_type": "my_widget",
78+
"name": "My Widget",
79+
"entry": "index.js",
80+
"capabilities": ["read_metrics", "automation_trigger"],
81+
"sdk_version": "2.0.0"
82+
}
83+
```
84+
85+
3. If you want to call the local API, add `"local_api_call"` to `capabilities` and use `context.channel.localApiCall(...)`.
86+
4. Optional: add `csp` for a custom Content Security Policy string, or `signature` for an SHA-256 integrity check of the entry file.
87+
88+
## Additional optional fields
89+
90+
| Field | Type | Description |
91+
|---|---|---|
92+
| `sdk_version` | `string` | Widget SDK version the widget targets |
93+
| `csp` | `string` | Content Security Policy hint |
94+
| `signature` | `string` | Hex SHA-256 digest of the entry file for integrity verification |
95+
96+
## Backward compatibility
97+
98+
- TimeLens 2.0.0 still loads v1 manifests and auto-upgrades them.
99+
- Existing installed widgets do not need to be re-imported.
100+
- The Widget Dev Harness (available in dev mode) can load both v1 and v2 manifests from a local folder without installation.

examples/third-party-widget-template/README.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,50 @@ This template demonstrates the minimum files required to run a third-party JS wi
44

55
## Files
66

7-
- `manifest.json`: widget metadata and registry declaration
7+
- `manifest.json`: widget metadata and registry declaration (Widget SDK v2)
88
- `index.js`: ESM widget entry implementing `createWidget().mount/unmount`
9+
- `index.ts`: TypeScript source for the same widget (optional)
10+
- `package.json`: build scripts; run `npm install && npm run build` to compile `index.ts` to `dist/index.js`
11+
- `tsconfig.json`: TypeScript compiler options
12+
13+
## Manifest v2
14+
15+
```json
16+
{
17+
"manifest_version": 2,
18+
"widget_type": "sample_hello",
19+
"name": "Sample Hello Widget",
20+
"entry": "index.js",
21+
"capabilities": ["read_metrics", "automation_trigger"]
22+
}
23+
```
24+
25+
### Capabilities
26+
27+
| Capability | Runtime permissions granted |
28+
|---|---|
29+
| `read_metrics` | `screen-time:read`, `todo:read` |
30+
| `write_data` | `todo:write`, `settings:write` |
31+
| `automation_trigger` | `active-window:subscribe` |
32+
| `local_api_call` | `local-api:call` |
933

1034
## How to test
1135

1236
1. Copy this folder to your local TimeLens app data widgets directory:
13-
- `widgets/third-party-widget-template/`
37+
- `widgets/sample_hello/`
1438
2. Start TimeLens.
15-
3. Open Widget Center -> Add Widgets.
39+
3. Open Widget Center Add Widgets.
1640
4. Add `Sample Hello Widget` and open it.
1741

42+
For faster iteration, use the Widget Dev Harness (dev mode only):
43+
44+
1. Open Widget Center → "Dev Harness".
45+
2. Select this template folder.
46+
3. Toggle capabilities and reload instantly.
47+
1848
## Notes
1949

20-
- The current prototype supports local loading only.
2150
- Keep `widget_type` unique across all installed widgets.
2251
- The entry file must be valid ESM and export `createWidget()` or `mount()`.
52+
- Use `context.channel.localApiCall({ method, path, scopes })` to call the TimeLens local HTTP API.
53+
- See `docs/WIDGET_SDK_v2_MIGRATION.md` for migration from v1 manifests.

examples/third-party-widget-template/README.zh-CN.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,50 @@
44

55
## 文件说明
66

7-
- `manifest.json`:小组件元数据与注册声明
7+
- `manifest.json`:小组件元数据与注册声明(Widget SDK v2)
88
- `index.js`:ESM 入口,需实现 `createWidget().mount/unmount`
9+
- `index.ts`:同一小组件的 TypeScript 源码(可选)
10+
- `package.json`:构建脚本;运行 `npm install && npm run build` 可将 `index.ts` 编译为 `dist/index.js`
11+
- `tsconfig.json`:TypeScript 编译选项
12+
13+
## Manifest v2
14+
15+
```json
16+
{
17+
"manifest_version": 2,
18+
"widget_type": "sample_hello",
19+
"name": "Sample Hello Widget",
20+
"entry": "index.js",
21+
"capabilities": ["read_metrics", "automation_trigger"]
22+
}
23+
```
24+
25+
### 能力(Capabilities)
26+
27+
| 能力 | 授予的运行时权限 |
28+
|---|---|
29+
| `read_metrics` | `screen-time:read``todo:read` |
30+
| `write_data` | `todo:write``settings:write` |
31+
| `automation_trigger` | `active-window:subscribe` |
32+
| `local_api_call` | `local-api:call` |
933

1034
## 测试步骤
1135

1236
1. 将本目录复制到本机 TimeLens 应用数据 widgets 目录,例如:
13-
- `widgets/third-party-widget-template/`
37+
- `widgets/sample_hello/`
1438
2. 启动 TimeLens。
15-
3. 打开小组件中心 -> 添加小组件。
39+
3. 打开小组件中心 添加小组件。
1640
4. 添加 `Sample Hello Widget` 并打开。
1741

42+
如需快速迭代,可使用小组件开发调试台(仅开发模式):
43+
44+
1. 打开小组件中心 → 「开发调试」。
45+
2. 选择本模板文件夹。
46+
3. 切换能力并即时重载。
47+
1848
## 说明
1949

20-
- 当前雏形仅支持本地目录加载。
2150
- `widget_type` 必须在本地已安装小组件中唯一。
2251
- 入口文件必须是有效 ESM,且导出 `createWidget()``mount()`
52+
- 通过 `context.channel.localApiCall({ method, path, scopes })` 调用 TimeLens 本地 HTTP API。
53+
- 从 v1 清单迁移请参考 `docs/WIDGET_SDK_v2_MIGRATION.md`

examples/third-party-widget-template/index.js

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,60 @@
1+
/**
2+
* @typedef {Object} AppUsageSummary
3+
* @property {string} app_name
4+
* @property {string} exe_path
5+
* @property {number} total_seconds
6+
*/
7+
8+
/**
9+
* @typedef {Object} ActiveWindowInfo
10+
* @property {string} app_name
11+
* @property {string} exe_path
12+
* @property {string} window_title
13+
* @property {string} timestamp
14+
*/
15+
16+
/**
17+
* @typedef {Object} LocalApiCallOptions
18+
* @property {string} method - HTTP method, e.g. "GET" or "POST"
19+
* @property {string} path - API path, e.g. "/api/screen-time/today"
20+
* @property {unknown} [body] - JSON-serializable request body
21+
* @property {string[]} [scopes] - Required token scopes, e.g. ["screen-time:read"]
22+
*/
23+
24+
/**
25+
* @typedef {Object} WidgetChannel
26+
* @property {() => Promise<AppUsageSummary[]>} getTodayAppTotals
27+
* @property {(start: string, end: string) => Promise<AppUsageSummary[]>} getAppTotalsInRange
28+
* @property {(cb: (info: ActiveWindowInfo) => void) => Promise<() => void>} onActiveWindowChanged
29+
* @property {(options: LocalApiCallOptions) => Promise<unknown>} localApiCall
30+
*/
31+
32+
/**
33+
* @typedef {Object} WidgetContext
34+
* @property {string} widgetId
35+
* @property {string} widgetType
36+
* @property {WidgetChannel} channel
37+
*/
38+
39+
/**
40+
* Create a new widget instance.
41+
* @returns {{
42+
* mount: (container: HTMLElement, context: WidgetContext) => Promise<void>,
43+
* unmount: () => Promise<void>
44+
* }}
45+
*/
146
export function createWidget() {
47+
/** @type {HTMLElement | null} */
248
let rootEl = null;
49+
/** @type {(() => void) | null} */
350
let stopListening = null;
451

552
return {
53+
/**
54+
* Mount the widget into the provided container.
55+
* @param {HTMLElement} container
56+
* @param {WidgetContext} context
57+
*/
658
async mount(container, context) {
759
rootEl = document.createElement("div");
860
rootEl.style.height = "100%";
@@ -23,8 +75,14 @@ export function createWidget() {
2375
usage.style.fontSize = "12px";
2476
usage.textContent = "Loading today's usage...";
2577

78+
const apiStatus = document.createElement("div");
79+
apiStatus.style.fontSize = "11px";
80+
apiStatus.style.opacity = "0.7";
81+
apiStatus.textContent = "localApiCall not started";
82+
2683
rootEl.appendChild(title);
2784
rootEl.appendChild(usage);
85+
rootEl.appendChild(apiStatus);
2886
container.appendChild(rootEl);
2987

3088
try {
@@ -36,9 +94,27 @@ export function createWidget() {
3694
usage.textContent = `Failed to load usage: ${String(err)}`;
3795
}
3896

39-
stopListening = await context.channel.onActiveWindowChanged((info) => {
40-
title.textContent = `Sample Hello Widget · ${info.app_name || "Unknown"}`;
41-
});
97+
try {
98+
stopListening = await context.channel.onActiveWindowChanged((info) => {
99+
title.textContent = `Sample Hello Widget · ${info.app_name || "Unknown"}`;
100+
});
101+
} catch (err) {
102+
title.textContent = `Sample Hello Widget · active window unavailable`;
103+
}
104+
105+
// Example: call the local HTTP API through the widget bridge.
106+
try {
107+
const result = await context.channel.localApiCall({
108+
method: "GET",
109+
path: "/api/screen-time/today",
110+
scopes: ["screen-time:read"],
111+
});
112+
const data = Array.isArray(result) ? result : [];
113+
const total = data.reduce((acc, row) => acc + (row.total_seconds || 0), 0);
114+
apiStatus.textContent = `localApiCall OK · ${data.length} apps · ${(total / 3600).toFixed(1)} h`;
115+
} catch (err) {
116+
apiStatus.textContent = `localApiCall: ${String(err)}`;
117+
}
42118
},
43119

44120
async unmount() {

0 commit comments

Comments
 (0)