|
1 | 1 | import * as vscode from "vscode"; |
2 | | -import * as path from "node:path"; |
3 | | -import * as fs from "node:fs"; |
4 | 2 |
|
5 | 3 | /** Fetch JSON from the TimeLens local API, returns null on any error. */ |
6 | 4 | async function fetchJson<T>(url: string): Promise<T | null> { |
@@ -59,6 +57,8 @@ export class DashboardPanel { |
59 | 57 | } |
60 | 58 | ); |
61 | 59 |
|
| 60 | + panel.webview.html = buildLoadingHtml(); |
| 61 | + |
62 | 62 | try { |
63 | 63 | panel.iconPath = iconPath; |
64 | 64 | } catch { |
@@ -88,48 +88,56 @@ export class DashboardPanel { |
88 | 88 | // Auto-refresh every 30 s while panel is visible |
89 | 89 | this.refreshTimer = setInterval(() => { |
90 | 90 | if (this.panel.visible) { |
91 | | - this.refresh(); |
| 91 | + void this.refresh(); |
92 | 92 | } |
93 | 93 | }, 30_000); |
94 | 94 |
|
95 | | - this.refresh(); |
| 95 | + void this.refresh().catch((error) => { |
| 96 | + this.panel.webview.html = buildErrorHtml(error); |
| 97 | + }); |
96 | 98 | } |
97 | 99 |
|
98 | 100 | public async refresh(): Promise<void> { |
| 101 | + this.panel.webview.html = buildLoadingHtml(); |
| 102 | + |
99 | 103 | const apiBase = vscode.workspace |
100 | 104 | .getConfiguration("timelens") |
101 | 105 | .get<string>("apiBaseUrl", "http://127.0.0.1:49152"); |
102 | 106 |
|
103 | | - const [todayStats, todayApps, langStats, projectStats, trackingStatus, appStatus] = |
104 | | - await Promise.all([ |
105 | | - fetchJson<{ total_seconds: number; session_count: number }>( |
106 | | - `${apiBase}/api/vscode/stats/today` |
107 | | - ), |
108 | | - fetchJson<{ app_name: string; exe_name: string; total_seconds: number; category: string }[]>( |
109 | | - `${apiBase}/api/screen-time/today` |
110 | | - ), |
111 | | - fetchJson<{ language: string; total_seconds: number }[]>( |
112 | | - `${apiBase}/api/vscode/languages/range?start=${today()}&end=${today()}` |
113 | | - ), |
114 | | - fetchJson<{ project_name: string; project_path: string; total_seconds: number; session_count: number }[]>( |
115 | | - `${apiBase}/api/vscode/projects/range?start=${today()}&end=${today()}` |
116 | | - ), |
117 | | - fetchJson<{ enabled: boolean; tracking_level?: string }>( |
118 | | - `${apiBase}/api/vscode/enabled` |
119 | | - ), |
120 | | - fetchJson<{ version: string; focus_active: boolean }>( |
121 | | - `${apiBase}/api/status` |
122 | | - ), |
123 | | - ]); |
124 | | - |
125 | | - this.panel.webview.html = buildHtml({ |
126 | | - todayStats, |
127 | | - todayApps: todayApps ?? [], |
128 | | - langStats: langStats ?? [], |
129 | | - projectStats: projectStats ?? [], |
130 | | - trackingStatus, |
131 | | - appStatus, |
132 | | - }); |
| 107 | + try { |
| 108 | + const [todayStats, todayApps, langStats, projectStats, trackingStatus, appStatus] = |
| 109 | + await Promise.all([ |
| 110 | + fetchJson<{ total_seconds: number; session_count: number }>( |
| 111 | + `${apiBase}/api/vscode/stats/today` |
| 112 | + ), |
| 113 | + fetchJson<{ app_name: string; exe_path: string; total_seconds: number }[]>( |
| 114 | + `${apiBase}/api/screen-time/today` |
| 115 | + ), |
| 116 | + fetchJson<{ language: string; total_seconds: number }[]>( |
| 117 | + `${apiBase}/api/vscode/languages/range?start=${today()}&end=${today()}` |
| 118 | + ), |
| 119 | + fetchJson<{ project_name: string; project_path: string; total_seconds: number; session_count: number }[]>( |
| 120 | + `${apiBase}/api/vscode/projects/range?start=${today()}&end=${today()}` |
| 121 | + ), |
| 122 | + fetchJson<{ enabled: boolean; tracking_level?: string }>( |
| 123 | + `${apiBase}/api/vscode/enabled` |
| 124 | + ), |
| 125 | + fetchJson<{ version: string; focus_active: boolean }>( |
| 126 | + `${apiBase}/api/status` |
| 127 | + ), |
| 128 | + ]); |
| 129 | + |
| 130 | + this.panel.webview.html = buildHtml({ |
| 131 | + todayStats, |
| 132 | + todayApps: todayApps ?? [], |
| 133 | + langStats: langStats ?? [], |
| 134 | + projectStats: projectStats ?? [], |
| 135 | + trackingStatus, |
| 136 | + appStatus, |
| 137 | + }); |
| 138 | + } catch (error) { |
| 139 | + this.panel.webview.html = buildErrorHtml(error); |
| 140 | + } |
133 | 141 | } |
134 | 142 |
|
135 | 143 | private async handleMessage(msg: { command: string; payload?: unknown }): Promise<void> { |
@@ -193,7 +201,7 @@ function today(): string { |
193 | 201 |
|
194 | 202 | interface BuildHtmlOptions { |
195 | 203 | todayStats: { total_seconds: number; session_count: number } | null; |
196 | | - todayApps: { app_name: string; exe_name: string; total_seconds: number; category: string }[]; |
| 204 | + todayApps: { app_name: string; exe_path: string; total_seconds: number }[]; |
197 | 205 | langStats: { language: string; total_seconds: number }[]; |
198 | 206 | projectStats: { project_name: string; project_path: string; total_seconds: number; session_count: number }[]; |
199 | 207 | trackingStatus: { enabled: boolean; tracking_level?: string } | null; |
@@ -221,7 +229,7 @@ function buildHtml(opts: BuildHtmlOptions): string { |
221 | 229 | const pct = totalAppSeconds > 0 ? Math.round((app.total_seconds / totalAppSeconds) * 100) : 0; |
222 | 230 | return `<div class="bar-row"> |
223 | 231 | <div class="bar-label"> |
224 | | - <span class="app-name" title="${esc(app.exe_name)}">${esc(app.app_name || app.exe_name)}</span> |
| 232 | + <span class="app-name" title="${esc(app.exe_path)}">${esc(app.app_name || app.exe_path)}</span> |
225 | 233 | <span class="bar-dur">${fmtDuration(app.total_seconds)}</span> |
226 | 234 | </div> |
227 | 235 | <div class="bar-track"><div class="bar-fill bar-fill-blue" style="width:${pct}%"></div></div> |
@@ -419,6 +427,65 @@ ${topApps.length > 0 ? ` |
419 | 427 | </html>`; |
420 | 428 | } |
421 | 429 |
|
422 | | -function esc(s: string): string { |
423 | | - return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); |
| 430 | +function esc(s: unknown): string { |
| 431 | + const value = typeof s === "string" ? s : String(s ?? ""); |
| 432 | + return value |
| 433 | + .replace(/&/g, "&") |
| 434 | + .replace(/</g, "<") |
| 435 | + .replace(/>/g, ">") |
| 436 | + .replace(/"/g, """); |
| 437 | +} |
| 438 | + |
| 439 | +function buildLoadingHtml(): string { |
| 440 | + return `<!DOCTYPE html> |
| 441 | +<html lang="en"> |
| 442 | +<head> |
| 443 | + <meta charset="UTF-8"> |
| 444 | + <meta name="viewport" content="width=device-width,initial-scale=1"> |
| 445 | + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline';"> |
| 446 | + <style> |
| 447 | + body { font-family: var(--vscode-font-family); padding: 16px; color: var(--vscode-foreground); background: var(--vscode-editor-background); } |
| 448 | + .card { border: 1px solid var(--vscode-panel-border); border-radius: 8px; padding: 12px; background: var(--vscode-sideBar-background); } |
| 449 | + .title { font-size: 16px; font-weight: 700; margin-bottom: 8px; } |
| 450 | + .muted { color: var(--vscode-descriptionForeground); font-size: 12px; } |
| 451 | + </style> |
| 452 | +</head> |
| 453 | +<body> |
| 454 | + <div class="card"> |
| 455 | + <div class="title">TimeLens Dashboard</div> |
| 456 | + <div class="muted">Loading...</div> |
| 457 | + </div> |
| 458 | +</body> |
| 459 | +</html>`; |
| 460 | +} |
| 461 | + |
| 462 | +function buildErrorHtml(error: unknown): string { |
| 463 | + const message = error instanceof Error ? error.message : "Unknown error"; |
| 464 | + return `<!DOCTYPE html> |
| 465 | +<html lang="en"> |
| 466 | +<head> |
| 467 | + <meta charset="UTF-8"> |
| 468 | + <meta name="viewport" content="width=device-width,initial-scale=1"> |
| 469 | + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline';"> |
| 470 | + <style> |
| 471 | + body { font-family: var(--vscode-font-family); padding: 16px; color: var(--vscode-foreground); background: var(--vscode-editor-background); } |
| 472 | + .card { border: 1px solid var(--vscode-panel-border); border-radius: 8px; padding: 12px; background: var(--vscode-sideBar-background); } |
| 473 | + .title { font-size: 16px; font-weight: 700; margin-bottom: 8px; } |
| 474 | + .muted { color: var(--vscode-descriptionForeground); font-size: 12px; } |
| 475 | + button { margin-top: 12px; border: 1px solid var(--vscode-panel-border); border-radius: 6px; padding: 6px 10px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); cursor: pointer; } |
| 476 | + </style> |
| 477 | +</head> |
| 478 | +<body> |
| 479 | + <div class="card"> |
| 480 | + <div class="title">TimeLens Dashboard</div> |
| 481 | + <div class="muted">页面加载失败,但扩展仍在运行。</div> |
| 482 | + <div class="muted" style="margin-top:8px;">${esc(message)}</div> |
| 483 | + <button onclick="refresh()">重试</button> |
| 484 | + </div> |
| 485 | + <script> |
| 486 | + const vscode = acquireVsCodeApi(); |
| 487 | + function refresh() { vscode.postMessage({ command: 'refresh' }); } |
| 488 | + </script> |
| 489 | +</body> |
| 490 | +</html>`; |
424 | 491 | } |
0 commit comments