dash-devtools 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. dash_devtools/__init__.py +8 -0
  2. dash_devtools/__main__.py +11 -0
  3. dash_devtools/ai_engine.py +441 -0
  4. dash_devtools/browser.py +541 -0
  5. dash_devtools/cli.py +1452 -0
  6. dash_devtools/database.py +338 -0
  7. dash_devtools/dbdiagram.py +183 -0
  8. dash_devtools/e2e.py +329 -0
  9. dash_devtools/fixers/__init__.py +57 -0
  10. dash_devtools/fixers/migration_fixer.py +115 -0
  11. dash_devtools/fixers/ux_fixer.py +106 -0
  12. dash_devtools/fixers/version_bumper.py +115 -0
  13. dash_devtools/gas_mes_test.py +1241 -0
  14. dash_devtools/generators/__init__.py +84 -0
  15. dash_devtools/health.py +476 -0
  16. dash_devtools/hooks/__init__.py +250 -0
  17. dash_devtools/hooks/pre_commit.py +161 -0
  18. dash_devtools/hooks/pre_push.py +275 -0
  19. dash_devtools/init_test.py +352 -0
  20. dash_devtools/markdown_report.py +309 -0
  21. dash_devtools/migrators/__init__.py +21 -0
  22. dash_devtools/perf.py +321 -0
  23. dash_devtools/report.py +667 -0
  24. dash_devtools/reporters/__init__.py +11 -0
  25. dash_devtools/spec.py +230 -0
  26. dash_devtools/stats.py +355 -0
  27. dash_devtools/test_suite.py +690 -0
  28. dash_devtools/testing.py +416 -0
  29. dash_devtools/validators/__init__.py +157 -0
  30. dash_devtools/validators/backend/__init__.py +12 -0
  31. dash_devtools/validators/backend/nodejs.py +245 -0
  32. dash_devtools/validators/backend/python.py +439 -0
  33. dash_devtools/validators/code_quality.py +243 -0
  34. dash_devtools/validators/common/__init__.py +11 -0
  35. dash_devtools/validators/common/quality.py +319 -0
  36. dash_devtools/validators/common/security.py +270 -0
  37. dash_devtools/validators/common/spec.py +273 -0
  38. dash_devtools/validators/detector.py +394 -0
  39. dash_devtools/validators/frontend/__init__.py +14 -0
  40. dash_devtools/validators/frontend/angular.py +245 -0
  41. dash_devtools/validators/frontend/gas.py +310 -0
  42. dash_devtools/validators/frontend/vite.py +539 -0
  43. dash_devtools/validators/migration.py +292 -0
  44. dash_devtools/validators/performance.py +167 -0
  45. dash_devtools/validators/security.py +205 -0
  46. dash_devtools/vision/__init__.py +368 -0
  47. dash_devtools/watch.py +266 -0
  48. dash_devtools/word_report.py +690 -0
  49. dash_devtools-1.0.0.dist-info/METADATA +834 -0
  50. dash_devtools-1.0.0.dist-info/RECORD +53 -0
  51. dash_devtools-1.0.0.dist-info/WHEEL +5 -0
  52. dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
  53. dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,667 @@
1
+ """
2
+ 整合報告產生器
3
+
4
+ 產生完整的專案報告,包含:
5
+ - 健康評分
6
+ - 程式碼統計
7
+ - UI 截圖
8
+ - 測試結果
9
+ - HTML 報告輸出
10
+ """
11
+
12
+ import json
13
+ import subprocess
14
+ import base64
15
+ from pathlib import Path
16
+ from datetime import datetime
17
+ from dataclasses import dataclass, field
18
+ from typing import Dict, List, Optional
19
+ from rich.console import Console
20
+ from rich.panel import Panel
21
+ from rich.text import Text
22
+ from rich.progress import Progress, SpinnerColumn, TextColumn
23
+
24
+ console = Console()
25
+
26
+
27
+ @dataclass
28
+ class TestResult:
29
+ """測試結果"""
30
+ framework: str # pytest, jest, vitest, etc.
31
+ passed: int = 0
32
+ failed: int = 0
33
+ skipped: int = 0
34
+ duration: float = 0.0
35
+ output: str = ""
36
+ success: bool = True
37
+
38
+
39
+ @dataclass
40
+ class ScreenshotResult:
41
+ """截圖結果"""
42
+ url: str
43
+ path: str
44
+ success: bool = True
45
+ error: str = ""
46
+
47
+
48
+ @dataclass
49
+ class ReportData:
50
+ """報告資料"""
51
+ project_name: str
52
+ generated_at: str
53
+ health_scores: Dict = field(default_factory=dict)
54
+ stats: Dict = field(default_factory=dict)
55
+ test_result: Optional[TestResult] = None
56
+ screenshots: List[ScreenshotResult] = field(default_factory=list)
57
+
58
+
59
+ class ReportGenerator:
60
+ """報告產生器"""
61
+
62
+ def __init__(self, project_path: str):
63
+ self.project_path = Path(project_path)
64
+ self.project_name = self.project_path.name
65
+ self.report_dir = self.project_path / 'reports'
66
+ self.report_data = ReportData(
67
+ project_name=self.project_name,
68
+ generated_at=datetime.now().isoformat()
69
+ )
70
+
71
+ def collect_health(self) -> Dict:
72
+ """收集健康評分"""
73
+ from .health import HealthChecker
74
+
75
+ checker = HealthChecker(str(self.project_path))
76
+ scores = checker.check_all()
77
+
78
+ health_data = {
79
+ 'total_score': sum(s.score for s in scores.values()) // len(scores),
80
+ 'scores': {}
81
+ }
82
+
83
+ for key, score in scores.items():
84
+ health_data['scores'][key] = {
85
+ 'score': score.score,
86
+ 'category': score.category,
87
+ 'issues': score.issues,
88
+ 'recommendations': score.recommendations
89
+ }
90
+
91
+ self.report_data.health_scores = health_data
92
+ return health_data
93
+
94
+ def collect_stats(self) -> Dict:
95
+ """收集程式碼統計"""
96
+ from .stats import StatsCollector
97
+
98
+ collector = StatsCollector(str(self.project_path))
99
+ stats = collector.collect()
100
+
101
+ stats_data = {
102
+ 'total_files': stats.total_files,
103
+ 'total_lines': stats.total_lines,
104
+ 'total_code_lines': stats.total_code_lines,
105
+ 'size_bytes': stats.total_size_bytes,
106
+ 'languages': {},
107
+ 'largest_files': stats.largest_files[:5],
108
+ 'complexity_issues': stats.complexity_issues
109
+ }
110
+
111
+ for name, lang in stats.languages.items():
112
+ stats_data['languages'][name] = {
113
+ 'files': lang.files,
114
+ 'lines': lang.lines
115
+ }
116
+
117
+ self.report_data.stats = stats_data
118
+ return stats_data
119
+
120
+ def run_tests(self) -> TestResult:
121
+ """執行測試"""
122
+ result = TestResult(framework='unknown')
123
+
124
+ # 偵測測試框架
125
+ package_json = self.project_path / 'package.json'
126
+ if package_json.exists():
127
+ try:
128
+ pkg = json.loads(package_json.read_text())
129
+ deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
130
+ scripts = pkg.get('scripts', {})
131
+
132
+ # 判斷測試框架
133
+ if 'vitest' in deps:
134
+ result.framework = 'vitest'
135
+ elif 'jest' in deps:
136
+ result.framework = 'jest'
137
+ elif '@angular-devkit/build-angular' in deps:
138
+ result.framework = 'karma'
139
+
140
+ # 執行測試
141
+ if 'test' in scripts:
142
+ try:
143
+ proc = subprocess.run(
144
+ ['npm', 'test', '--', '--passWithNoTests', '--run'],
145
+ cwd=self.project_path,
146
+ capture_output=True,
147
+ text=True,
148
+ timeout=120
149
+ )
150
+ result.output = proc.stdout + proc.stderr
151
+ result.success = proc.returncode == 0
152
+
153
+ # 簡單解析結果
154
+ if 'passed' in result.output.lower():
155
+ result.passed = result.output.lower().count('passed')
156
+ if 'failed' in result.output.lower():
157
+ result.failed = result.output.lower().count('failed')
158
+
159
+ except subprocess.TimeoutExpired:
160
+ result.success = False
161
+ result.output = "測試超時 (120秒)"
162
+ except Exception as e:
163
+ result.success = False
164
+ result.output = str(e)
165
+
166
+ except Exception:
167
+ pass
168
+
169
+ # Python 專案
170
+ requirements = self.project_path / 'requirements.txt'
171
+ pytest_ini = self.project_path / 'pytest.ini'
172
+ if requirements.exists() or pytest_ini.exists():
173
+ result.framework = 'pytest'
174
+ try:
175
+ proc = subprocess.run(
176
+ ['python', '-m', 'pytest', '--tb=short', '-q'],
177
+ cwd=self.project_path,
178
+ capture_output=True,
179
+ text=True,
180
+ timeout=120
181
+ )
182
+ result.output = proc.stdout + proc.stderr
183
+ result.success = proc.returncode == 0
184
+
185
+ # 解析 pytest 結果
186
+ import re
187
+ match = re.search(r'(\d+) passed', result.output)
188
+ if match:
189
+ result.passed = int(match.group(1))
190
+ match = re.search(r'(\d+) failed', result.output)
191
+ if match:
192
+ result.failed = int(match.group(1))
193
+
194
+ except Exception as e:
195
+ result.output = str(e)
196
+ result.success = False
197
+
198
+ self.report_data.test_result = result
199
+ return result
200
+
201
+ def take_screenshots(self, urls: List[str] = None) -> List[ScreenshotResult]:
202
+ """使用 agent-browser 截圖"""
203
+ import shutil
204
+ results = []
205
+
206
+ # 檢查 agent-browser 是否安裝
207
+ if not shutil.which('agent-browser'):
208
+ console.print("[yellow]agent-browser 未安裝,跳過截圖[/yellow]")
209
+ console.print("[dim]安裝方式: npm install -g agent-browser && agent-browser install[/dim]")
210
+ return results
211
+
212
+ # 如果沒指定 URL,嘗試偵測本地開發伺服器
213
+ if not urls:
214
+ package_json = self.project_path / 'package.json'
215
+ if package_json.exists():
216
+ try:
217
+ pkg = json.loads(package_json.read_text())
218
+ scripts = pkg.get('scripts', {})
219
+ if 'dev' in scripts or 'start' in scripts:
220
+ urls = ['http://localhost:5173', 'http://localhost:3000', 'http://localhost:4200']
221
+ except Exception:
222
+ pass
223
+
224
+ if not urls:
225
+ return results
226
+
227
+ # 確保報告目錄存在
228
+ screenshots_dir = self.report_dir / 'screenshots'
229
+ screenshots_dir.mkdir(parents=True, exist_ok=True)
230
+
231
+ # 使用 agent-browser 截圖
232
+ for url in urls:
233
+ screenshot_path = screenshots_dir / f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
234
+
235
+ try:
236
+ # 開啟頁面
237
+ open_result = subprocess.run(
238
+ ['agent-browser', 'open', url],
239
+ capture_output=True,
240
+ text=True,
241
+ timeout=30
242
+ )
243
+
244
+ if open_result.returncode != 0:
245
+ results.append(ScreenshotResult(
246
+ url=url,
247
+ path="",
248
+ success=False,
249
+ error=f"無法開啟頁面: {open_result.stderr}"
250
+ ))
251
+ subprocess.run(['agent-browser', 'close'], capture_output=True)
252
+ continue
253
+
254
+ # 等待頁面載入
255
+ subprocess.run(
256
+ ['agent-browser', 'wait', '--load', 'networkidle'],
257
+ capture_output=True,
258
+ timeout=15
259
+ )
260
+
261
+ # 額外等待 JS 渲染
262
+ subprocess.run(
263
+ ['agent-browser', 'wait', '2000'],
264
+ capture_output=True,
265
+ timeout=5
266
+ )
267
+
268
+ # 截圖
269
+ screenshot_result = subprocess.run(
270
+ ['agent-browser', 'screenshot', str(screenshot_path), '--full'],
271
+ capture_output=True,
272
+ text=True,
273
+ timeout=30
274
+ )
275
+
276
+ # 關閉瀏覽器
277
+ subprocess.run(['agent-browser', 'close'], capture_output=True)
278
+
279
+ if screenshot_result.returncode == 0 and screenshot_path.exists():
280
+ results.append(ScreenshotResult(
281
+ url=url,
282
+ path=str(screenshot_path),
283
+ success=True
284
+ ))
285
+ else:
286
+ results.append(ScreenshotResult(
287
+ url=url,
288
+ path="",
289
+ success=False,
290
+ error=screenshot_result.stderr or "截圖失敗"
291
+ ))
292
+
293
+ except subprocess.TimeoutExpired:
294
+ subprocess.run(['agent-browser', 'close'], capture_output=True)
295
+ results.append(ScreenshotResult(
296
+ url=url,
297
+ path="",
298
+ success=False,
299
+ error="操作超時"
300
+ ))
301
+ except Exception as e:
302
+ subprocess.run(['agent-browser', 'close'], capture_output=True)
303
+ results.append(ScreenshotResult(
304
+ url=url,
305
+ path="",
306
+ success=False,
307
+ error=str(e)
308
+ ))
309
+
310
+ self.report_data.screenshots = results
311
+ return results
312
+
313
+ def generate_html_report(self) -> str:
314
+ """產生 HTML 報告"""
315
+ self.report_dir.mkdir(parents=True, exist_ok=True)
316
+ report_path = self.report_dir / f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
317
+
318
+ # 取得評分顏色
319
+ def get_color(score):
320
+ if score >= 90:
321
+ return '#22c55e' # green
322
+ elif score >= 70:
323
+ return '#eab308' # yellow
324
+ elif score >= 50:
325
+ return '#f97316' # orange
326
+ else:
327
+ return '#ef4444' # red
328
+
329
+ # 語言分佈圖資料
330
+ languages_data = self.report_data.stats.get('languages', {})
331
+ total_lines = self.report_data.stats.get('total_lines', 1)
332
+
333
+ lang_bars = ""
334
+ colors = ['#3b82f6', '#22c55e', '#eab308', '#ef4444', '#8b5cf6', '#06b6d4', '#f97316', '#ec4899']
335
+ for i, (name, data) in enumerate(sorted(languages_data.items(), key=lambda x: x[1]['lines'], reverse=True)[:6]):
336
+ pct = (data['lines'] / total_lines) * 100
337
+ color = colors[i % len(colors)]
338
+ lang_bars += f'''
339
+ <div class="lang-bar">
340
+ <span class="lang-name">{name}</span>
341
+ <div class="bar-container">
342
+ <div class="bar-fill" style="width: {pct}%; background: {color};"></div>
343
+ </div>
344
+ <span class="lang-pct">{pct:.1f}%</span>
345
+ </div>
346
+ '''
347
+
348
+ # 健康評分卡片
349
+ health = self.report_data.health_scores
350
+ total_score = health.get('total_score', 0)
351
+ score_cards = ""
352
+ for key, data in health.get('scores', {}).items():
353
+ score = data['score']
354
+ category = data['category']
355
+ score_cards += f'''
356
+ <div class="score-card">
357
+ <div class="score-value" style="color: {get_color(score)}">{score}</div>
358
+ <div class="score-label">{category}</div>
359
+ </div>
360
+ '''
361
+
362
+ # 測試結果
363
+ test_html = ""
364
+ if self.report_data.test_result:
365
+ tr = self.report_data.test_result
366
+ test_status = "PASS" if tr.success else "FAIL"
367
+ test_color = "#22c55e" if tr.success else "#ef4444"
368
+ test_html = f'''
369
+ <div class="section">
370
+ <h2>測試結果</h2>
371
+ <div class="test-result">
372
+ <div class="test-status" style="background: {test_color}">{test_status}</div>
373
+ <div class="test-stats">
374
+ <span class="passed">{tr.passed} passed</span>
375
+ <span class="failed">{tr.failed} failed</span>
376
+ </div>
377
+ <div class="test-framework">Framework: {tr.framework}</div>
378
+ </div>
379
+ </div>
380
+ '''
381
+
382
+ # 截圖
383
+ screenshots_html = ""
384
+ for ss in self.report_data.screenshots:
385
+ if ss.success and Path(ss.path).exists():
386
+ # 轉換為 base64
387
+ img_data = base64.b64encode(Path(ss.path).read_bytes()).decode()
388
+ screenshots_html += f'''
389
+ <div class="screenshot">
390
+ <div class="screenshot-url">{ss.url}</div>
391
+ <img src="data:image/png;base64,{img_data}" alt="Screenshot" />
392
+ </div>
393
+ '''
394
+
395
+ if screenshots_html:
396
+ screenshots_html = f'''
397
+ <div class="section">
398
+ <h2>UI 截圖</h2>
399
+ <div class="screenshots-grid">
400
+ {screenshots_html}
401
+ </div>
402
+ </div>
403
+ '''
404
+
405
+ # 問題與建議
406
+ issues_html = ""
407
+ recommendations_html = ""
408
+ for data in health.get('scores', {}).values():
409
+ for issue in data.get('issues', []):
410
+ issues_html += f'<li class="issue">{issue}</li>'
411
+ for rec in data.get('recommendations', []):
412
+ recommendations_html += f'<li class="recommendation">{rec}</li>'
413
+
414
+ html = f'''<!DOCTYPE html>
415
+ <html lang="zh-TW">
416
+ <head>
417
+ <meta charset="UTF-8">
418
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
419
+ <title>{self.project_name} - 專案報告</title>
420
+ <style>
421
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
422
+ body {{
423
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
424
+ background: #0f172a;
425
+ color: #e2e8f0;
426
+ line-height: 1.6;
427
+ padding: 2rem;
428
+ }}
429
+ .container {{ max-width: 1200px; margin: 0 auto; }}
430
+ .header {{
431
+ text-align: center;
432
+ padding: 2rem;
433
+ background: linear-gradient(135deg, #1e293b, #334155);
434
+ border-radius: 1rem;
435
+ margin-bottom: 2rem;
436
+ }}
437
+ .header h1 {{ font-size: 2rem; margin-bottom: 0.5rem; }}
438
+ .header .date {{ color: #94a3b8; font-size: 0.875rem; }}
439
+ .total-score {{
440
+ font-size: 4rem;
441
+ font-weight: bold;
442
+ color: {get_color(total_score)};
443
+ margin: 1rem 0;
444
+ }}
445
+ .section {{
446
+ background: #1e293b;
447
+ border-radius: 1rem;
448
+ padding: 1.5rem;
449
+ margin-bottom: 1.5rem;
450
+ }}
451
+ .section h2 {{
452
+ font-size: 1.25rem;
453
+ margin-bottom: 1rem;
454
+ color: #38bdf8;
455
+ }}
456
+ .scores-grid {{
457
+ display: grid;
458
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
459
+ gap: 1rem;
460
+ }}
461
+ .score-card {{
462
+ background: #334155;
463
+ padding: 1rem;
464
+ border-radius: 0.5rem;
465
+ text-align: center;
466
+ }}
467
+ .score-value {{ font-size: 2rem; font-weight: bold; }}
468
+ .score-label {{ color: #94a3b8; font-size: 0.875rem; }}
469
+ .stats-grid {{
470
+ display: grid;
471
+ grid-template-columns: repeat(4, 1fr);
472
+ gap: 1rem;
473
+ }}
474
+ .stat-item {{
475
+ background: #334155;
476
+ padding: 1rem;
477
+ border-radius: 0.5rem;
478
+ text-align: center;
479
+ }}
480
+ .stat-value {{ font-size: 1.5rem; font-weight: bold; color: #38bdf8; }}
481
+ .stat-label {{ color: #94a3b8; font-size: 0.75rem; }}
482
+ .lang-bar {{
483
+ display: flex;
484
+ align-items: center;
485
+ margin-bottom: 0.5rem;
486
+ }}
487
+ .lang-name {{ width: 100px; font-size: 0.875rem; }}
488
+ .bar-container {{
489
+ flex: 1;
490
+ height: 8px;
491
+ background: #334155;
492
+ border-radius: 4px;
493
+ overflow: hidden;
494
+ margin: 0 1rem;
495
+ }}
496
+ .bar-fill {{ height: 100%; border-radius: 4px; }}
497
+ .lang-pct {{ width: 60px; text-align: right; font-size: 0.875rem; color: #94a3b8; }}
498
+ .test-result {{
499
+ display: flex;
500
+ align-items: center;
501
+ gap: 1rem;
502
+ }}
503
+ .test-status {{
504
+ padding: 0.5rem 1rem;
505
+ border-radius: 0.25rem;
506
+ font-weight: bold;
507
+ }}
508
+ .test-stats .passed {{ color: #22c55e; }}
509
+ .test-stats .failed {{ color: #ef4444; margin-left: 1rem; }}
510
+ .test-framework {{ color: #94a3b8; margin-left: auto; }}
511
+ .issues-list, .recommendations-list {{
512
+ list-style: none;
513
+ padding-left: 0;
514
+ }}
515
+ .issue, .recommendation {{
516
+ padding: 0.5rem 1rem;
517
+ margin-bottom: 0.5rem;
518
+ border-radius: 0.25rem;
519
+ font-size: 0.875rem;
520
+ }}
521
+ .issue {{ background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; }}
522
+ .recommendation {{ background: rgba(234, 179, 8, 0.1); border-left: 3px solid #eab308; }}
523
+ .screenshots-grid {{ display: grid; gap: 1rem; }}
524
+ .screenshot img {{
525
+ max-width: 100%;
526
+ border-radius: 0.5rem;
527
+ border: 1px solid #334155;
528
+ }}
529
+ .screenshot-url {{
530
+ font-size: 0.75rem;
531
+ color: #94a3b8;
532
+ margin-bottom: 0.5rem;
533
+ }}
534
+ .footer {{
535
+ text-align: center;
536
+ color: #64748b;
537
+ font-size: 0.75rem;
538
+ margin-top: 2rem;
539
+ }}
540
+ </style>
541
+ </head>
542
+ <body>
543
+ <div class="container">
544
+ <div class="header">
545
+ <h1>{self.project_name}</h1>
546
+ <div class="date">報告產生時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</div>
547
+ <div class="total-score">{total_score}/100</div>
548
+ </div>
549
+
550
+ <div class="section">
551
+ <h2>健康評分</h2>
552
+ <div class="scores-grid">
553
+ {score_cards}
554
+ </div>
555
+ </div>
556
+
557
+ <div class="section">
558
+ <h2>程式碼統計</h2>
559
+ <div class="stats-grid">
560
+ <div class="stat-item">
561
+ <div class="stat-value">{self.report_data.stats.get('total_files', 0):,}</div>
562
+ <div class="stat-label">檔案數</div>
563
+ </div>
564
+ <div class="stat-item">
565
+ <div class="stat-value">{self.report_data.stats.get('total_lines', 0):,}</div>
566
+ <div class="stat-label">總行數</div>
567
+ </div>
568
+ <div class="stat-item">
569
+ <div class="stat-value">{self.report_data.stats.get('total_code_lines', 0):,}</div>
570
+ <div class="stat-label">程式碼行數</div>
571
+ </div>
572
+ <div class="stat-item">
573
+ <div class="stat-value">{self.report_data.stats.get('size_bytes', 0) / 1024:.0f} KB</div>
574
+ <div class="stat-label">大小</div>
575
+ </div>
576
+ </div>
577
+ </div>
578
+
579
+ <div class="section">
580
+ <h2>語言分佈</h2>
581
+ {lang_bars}
582
+ </div>
583
+
584
+ {test_html}
585
+
586
+ {screenshots_html}
587
+
588
+ <div class="section">
589
+ <h2>問題與建議</h2>
590
+ <h3 style="color: #ef4444; font-size: 0.875rem; margin-bottom: 0.5rem;">問題</h3>
591
+ <ul class="issues-list">{issues_html if issues_html else '<li style="color: #94a3b8;">無問題</li>'}</ul>
592
+ <h3 style="color: #eab308; font-size: 0.875rem; margin: 1rem 0 0.5rem;">建議</h3>
593
+ <ul class="recommendations-list">{recommendations_html if recommendations_html else '<li style="color: #94a3b8;">無建議</li>'}</ul>
594
+ </div>
595
+
596
+ <div class="footer">
597
+ Generated by DashAI DevTools v2.0
598
+ </div>
599
+ </div>
600
+ </body>
601
+ </html>'''
602
+
603
+ report_path.write_text(html, encoding='utf-8')
604
+ return str(report_path)
605
+
606
+
607
+ def run_report(project_path: str, include_tests: bool = True,
608
+ include_screenshots: bool = False, urls: List[str] = None,
609
+ open_browser: bool = True) -> dict:
610
+ """產生完整報告"""
611
+
612
+ generator = ReportGenerator(project_path)
613
+
614
+ with Progress(
615
+ SpinnerColumn(),
616
+ TextColumn("[progress.description]{task.description}"),
617
+ console=console
618
+ ) as progress:
619
+
620
+ # 收集健康評分
621
+ task = progress.add_task("收集健康評分...", total=None)
622
+ generator.collect_health()
623
+ progress.update(task, description="[green]健康評分 ✓")
624
+
625
+ # 收集統計
626
+ task = progress.add_task("收集程式碼統計...", total=None)
627
+ generator.collect_stats()
628
+ progress.update(task, description="[green]程式碼統計 ✓")
629
+
630
+ # 執行測試
631
+ if include_tests:
632
+ task = progress.add_task("執行測試...", total=None)
633
+ test_result = generator.run_tests()
634
+ if test_result.framework != 'unknown':
635
+ status = "[green]✓" if test_result.success else "[red]✗"
636
+ progress.update(task, description=f"{status} 測試完成 ({test_result.framework})")
637
+ else:
638
+ progress.update(task, description="[yellow]跳過測試 (未偵測到測試框架)")
639
+
640
+ # 截圖
641
+ if include_screenshots:
642
+ task = progress.add_task("擷取截圖...", total=None)
643
+ screenshots = generator.take_screenshots(urls)
644
+ success_count = sum(1 for s in screenshots if s.success)
645
+ progress.update(task, description=f"[green]截圖完成 ({success_count}/{len(screenshots)})")
646
+
647
+ # 產生報告
648
+ task = progress.add_task("產生 HTML 報告...", total=None)
649
+ report_path = generator.generate_html_report()
650
+ progress.update(task, description="[green]報告已產生 ✓")
651
+
652
+ console.print()
653
+ console.print(f"[green]報告已產生:[/green] {report_path}")
654
+
655
+ # 開啟瀏覽器
656
+ if open_browser:
657
+ try:
658
+ import webbrowser
659
+ webbrowser.open(f'file://{report_path}')
660
+ except Exception:
661
+ pass
662
+
663
+ return {
664
+ 'success': True,
665
+ 'report_path': report_path,
666
+ 'health_score': generator.report_data.health_scores.get('total_score', 0)
667
+ }
@@ -0,0 +1,11 @@
1
+ """
2
+ 報告產生工具
3
+ """
4
+
5
+ __all__ = ['generate_html_report']
6
+
7
+
8
+ def generate_html_report(results, output_path):
9
+ """產生 HTML 報告"""
10
+ # TODO: 實作 HTML 報告產生
11
+ pass