greenmining 1.0.5__py3-none-any.whl → 1.0.7__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.
@@ -0,0 +1,246 @@
1
+ # Version-by-version power analysis.
2
+ # Measure power consumption across multiple software versions/tags.
3
+
4
+ from __future__ import annotations
5
+
6
+ import subprocess
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from greenmining.utils import colored_print
11
+
12
+
13
+ @dataclass
14
+ class VersionPowerProfile:
15
+ # Power profile for a single version.
16
+
17
+ version: str
18
+ commit_sha: str
19
+ energy_joules: float = 0.0
20
+ power_watts_avg: float = 0.0
21
+ duration_seconds: float = 0.0
22
+ iterations: int = 0
23
+ energy_std: float = 0.0 # Standard deviation across iterations
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ return {
27
+ "version": self.version,
28
+ "commit_sha": self.commit_sha,
29
+ "energy_joules": round(self.energy_joules, 4),
30
+ "power_watts_avg": round(self.power_watts_avg, 4),
31
+ "duration_seconds": round(self.duration_seconds, 4),
32
+ "iterations": self.iterations,
33
+ "energy_std": round(self.energy_std, 4),
34
+ }
35
+
36
+
37
+ @dataclass
38
+ class VersionPowerReport:
39
+ # Complete power analysis report across versions.
40
+
41
+ versions: List[VersionPowerProfile] = field(default_factory=list)
42
+ trend: str = "stable" # increasing, decreasing, stable
43
+ total_change_percent: float = 0.0
44
+ most_efficient: str = ""
45
+ least_efficient: str = ""
46
+
47
+ def to_dict(self) -> Dict[str, Any]:
48
+ return {
49
+ "versions": [v.to_dict() for v in self.versions],
50
+ "trend": self.trend,
51
+ "total_change_percent": round(self.total_change_percent, 2),
52
+ "most_efficient": self.most_efficient,
53
+ "least_efficient": self.least_efficient,
54
+ }
55
+
56
+ def summary(self) -> str:
57
+ # Generate human-readable summary.
58
+ lines = [
59
+ "Version Power Analysis Report",
60
+ "-" * 40,
61
+ f"Versions analyzed: {len(self.versions)}",
62
+ f"Trend: {self.trend}",
63
+ f"Total change: {self.total_change_percent:+.2f}%",
64
+ f"Most efficient: {self.most_efficient}",
65
+ f"Least efficient: {self.least_efficient}",
66
+ "",
67
+ "Per-version breakdown:",
68
+ ]
69
+ for v in self.versions:
70
+ lines.append(
71
+ f" {v.version}: {v.energy_joules:.4f}J "
72
+ f"({v.power_watts_avg:.2f}W avg, {v.duration_seconds:.2f}s)"
73
+ )
74
+ return "\n".join(lines)
75
+
76
+
77
+ class VersionPowerAnalyzer:
78
+ # Measure and compare power consumption across software versions.
79
+
80
+ def __init__(
81
+ self,
82
+ test_command: str = "pytest tests/",
83
+ energy_backend: str = "rapl",
84
+ iterations: int = 10,
85
+ warmup_iterations: int = 2,
86
+ ):
87
+ # Initialize version power analyzer.
88
+ # Args:
89
+ # test_command: Shell command to run for measurement
90
+ # energy_backend: Energy measurement backend
91
+ # iterations: Number of measurement iterations per version
92
+ # warmup_iterations: Warmup runs before measuring
93
+ self.test_command = test_command
94
+ self.energy_backend = energy_backend
95
+ self.iterations = iterations
96
+ self.warmup_iterations = warmup_iterations
97
+ self._meter = None
98
+
99
+ def _get_energy_meter(self):
100
+ # Get energy meter instance.
101
+ if self._meter is None:
102
+ from greenmining.energy.base import get_energy_meter
103
+
104
+ self._meter = get_energy_meter(self.energy_backend)
105
+ return self._meter
106
+
107
+ def _measure_version(self, repo_path: str, version: str) -> VersionPowerProfile:
108
+ # Measure power consumption for a specific version.
109
+ meter = self._get_energy_meter()
110
+
111
+ # Checkout version
112
+ sha = self._checkout_version(repo_path, version)
113
+ colored_print(f" Measuring {version} ({sha[:8]})...", "cyan")
114
+
115
+ # Warmup
116
+ for i in range(self.warmup_iterations):
117
+ subprocess.run(
118
+ self.test_command,
119
+ shell=True,
120
+ cwd=repo_path,
121
+ capture_output=True,
122
+ text=True,
123
+ timeout=600,
124
+ )
125
+
126
+ # Measure iterations
127
+ measurements = []
128
+ for i in range(self.iterations):
129
+ meter.start()
130
+ subprocess.run(
131
+ self.test_command,
132
+ shell=True,
133
+ cwd=repo_path,
134
+ capture_output=True,
135
+ text=True,
136
+ timeout=600,
137
+ )
138
+ metrics = meter.stop()
139
+ measurements.append(metrics)
140
+
141
+ # Aggregate
142
+ import numpy as np
143
+
144
+ energies = [m.joules for m in measurements]
145
+ avg_energy = float(np.mean(energies))
146
+ std_energy = float(np.std(energies))
147
+ avg_power = float(np.mean([m.watts_avg for m in measurements]))
148
+ avg_duration = float(np.mean([m.duration_seconds for m in measurements]))
149
+
150
+ profile = VersionPowerProfile(
151
+ version=version,
152
+ commit_sha=sha,
153
+ energy_joules=avg_energy,
154
+ power_watts_avg=avg_power,
155
+ duration_seconds=avg_duration,
156
+ iterations=self.iterations,
157
+ energy_std=std_energy,
158
+ )
159
+
160
+ colored_print(
161
+ f" {version}: {avg_energy:.4f}J +/-{std_energy:.4f} "
162
+ f"({avg_power:.2f}W, {avg_duration:.2f}s)",
163
+ "green",
164
+ )
165
+ return profile
166
+
167
+ def analyze_versions(
168
+ self,
169
+ repo_path: str,
170
+ versions: List[str],
171
+ ) -> VersionPowerReport:
172
+ # Analyze power consumption across multiple versions.
173
+ # Args:
174
+ # repo_path: Path to local git repository
175
+ # versions: List of version tags or commit references
176
+ colored_print(f"\nAnalyzing {len(versions)} versions for power consumption", "cyan")
177
+ colored_print(f" Test: {self.test_command}", "cyan")
178
+ colored_print(f" Iterations: {self.iterations} (+{self.warmup_iterations} warmup)", "cyan")
179
+
180
+ profiles = []
181
+ for version in versions:
182
+ try:
183
+ profile = self._measure_version(repo_path, version)
184
+ profiles.append(profile)
185
+ except Exception as e:
186
+ colored_print(f" Error measuring {version}: {e}", "red")
187
+
188
+ if not profiles:
189
+ return VersionPowerReport()
190
+
191
+ # Determine trend
192
+ first_energy = profiles[0].energy_joules
193
+ last_energy = profiles[-1].energy_joules
194
+
195
+ if first_energy > 0:
196
+ total_change = ((last_energy - first_energy) / first_energy) * 100
197
+ else:
198
+ total_change = 0.0
199
+
200
+ if total_change > 5:
201
+ trend = "increasing"
202
+ elif total_change < -5:
203
+ trend = "decreasing"
204
+ else:
205
+ trend = "stable"
206
+
207
+ # Find most/least efficient
208
+ most_efficient = min(profiles, key=lambda p: p.energy_joules)
209
+ least_efficient = max(profiles, key=lambda p: p.energy_joules)
210
+
211
+ report = VersionPowerReport(
212
+ versions=profiles,
213
+ trend=trend,
214
+ total_change_percent=total_change,
215
+ most_efficient=most_efficient.version,
216
+ least_efficient=least_efficient.version,
217
+ )
218
+
219
+ colored_print(f"\nTrend: {trend} ({total_change:+.2f}%)", "cyan")
220
+ colored_print(f"Most efficient: {most_efficient.version}", "green")
221
+ colored_print(f"Least efficient: {least_efficient.version}", "red")
222
+
223
+ # Restore to latest version
224
+ try:
225
+ self._checkout_version(repo_path, versions[-1])
226
+ except Exception:
227
+ pass
228
+
229
+ return report
230
+
231
+ @staticmethod
232
+ def _checkout_version(repo_path: str, version: str) -> str:
233
+ # Checkout a version and return its SHA.
234
+ subprocess.run(
235
+ ["git", "checkout", version, "--quiet"],
236
+ cwd=repo_path,
237
+ capture_output=True,
238
+ text=True,
239
+ )
240
+ result = subprocess.run(
241
+ ["git", "rev-parse", "HEAD"],
242
+ cwd=repo_path,
243
+ capture_output=True,
244
+ text=True,
245
+ )
246
+ return result.stdout.strip()
greenmining/config.py CHANGED
@@ -11,7 +11,8 @@ def _load_yaml_config(yaml_path: Path) -> Dict[str, Any]:
11
11
  return {}
12
12
  try:
13
13
  import yaml
14
- with open(yaml_path, 'r') as f:
14
+
15
+ with open(yaml_path, "r") as f:
15
16
  return yaml.safe_load(f) or {}
16
17
  except ImportError:
17
18
  return {}
@@ -30,7 +31,7 @@ class Config:
30
31
  load_dotenv(env_path)
31
32
  else:
32
33
  load_dotenv() # Load from system environment
33
-
34
+
34
35
  # Load YAML config (takes precedence for certain options)
35
36
  yaml_path = Path(yaml_file)
36
37
  self._yaml_config = _load_yaml_config(yaml_path)
@@ -45,22 +46,32 @@ class Config:
45
46
 
46
47
  # Search and Processing Configuration (YAML: sources.search.keywords)
47
48
  yaml_search = self._yaml_config.get("sources", {}).get("search", {})
48
- self.GITHUB_SEARCH_KEYWORDS = yaml_search.get("keywords",
49
- ["microservices", "microservice-architecture", "cloud-native"])
49
+ self.GITHUB_SEARCH_KEYWORDS = yaml_search.get(
50
+ "keywords", ["microservices", "microservice-architecture", "cloud-native"]
51
+ )
50
52
 
51
53
  # Supported Languages (YAML: sources.search.languages)
52
- self.SUPPORTED_LANGUAGES = yaml_search.get("languages", [
53
- "Java", "Python", "Go", "JavaScript", "TypeScript", "C#", "Rust",
54
- ])
54
+ self.SUPPORTED_LANGUAGES = yaml_search.get(
55
+ "languages",
56
+ [
57
+ "Java",
58
+ "Python",
59
+ "Go",
60
+ "JavaScript",
61
+ "TypeScript",
62
+ "C#",
63
+ "Rust",
64
+ ],
65
+ )
55
66
 
56
67
  # Repository and Commit Limits (YAML: extraction.*)
57
68
  yaml_extraction = self._yaml_config.get("extraction", {})
58
69
  self.MIN_STARS = yaml_search.get("min_stars", int(os.getenv("MIN_STARS", "100")))
59
70
  self.MAX_REPOS = int(os.getenv("MAX_REPOS", "100"))
60
- self.COMMITS_PER_REPO = yaml_extraction.get("max_commits",
61
- int(os.getenv("COMMITS_PER_REPO", "50")))
62
- self.DAYS_BACK = yaml_extraction.get("days_back",
63
- int(os.getenv("DAYS_BACK", "730")))
71
+ self.COMMITS_PER_REPO = yaml_extraction.get(
72
+ "max_commits", int(os.getenv("COMMITS_PER_REPO", "50"))
73
+ )
74
+ self.DAYS_BACK = yaml_extraction.get("days_back", int(os.getenv("DAYS_BACK", "730")))
64
75
  self.SKIP_MERGES = yaml_extraction.get("skip_merges", True)
65
76
 
66
77
  # Analysis Configuration (YAML: analysis.*)
@@ -72,14 +83,17 @@ class Config:
72
83
  self.TEMPORAL_GRANULARITY = os.getenv("TEMPORAL_GRANULARITY", "quarter")
73
84
  self.ENABLE_ML_FEATURES = os.getenv("ENABLE_ML_FEATURES", "false").lower() == "true"
74
85
  self.VALIDATION_SAMPLE_SIZE = int(os.getenv("VALIDATION_SAMPLE_SIZE", "30"))
75
-
86
+
76
87
  # PyDriller options (YAML: analysis.process_metrics, etc.)
77
- self.PROCESS_METRICS_ENABLED = yaml_analysis.get("process_metrics",
78
- os.getenv("PROCESS_METRICS_ENABLED", "true").lower() == "true")
79
- self.STRUCTURAL_METRICS_ENABLED = yaml_analysis.get("structural_metrics",
80
- os.getenv("STRUCTURAL_METRICS_ENABLED", "true").lower() == "true")
81
- self.DMM_ENABLED = yaml_analysis.get("delta_maintainability",
82
- os.getenv("DMM_ENABLED", "true").lower() == "true")
88
+ self.PROCESS_METRICS_ENABLED = yaml_analysis.get(
89
+ "process_metrics", os.getenv("PROCESS_METRICS_ENABLED", "true").lower() == "true"
90
+ )
91
+ self.STRUCTURAL_METRICS_ENABLED = yaml_analysis.get(
92
+ "structural_metrics", os.getenv("STRUCTURAL_METRICS_ENABLED", "true").lower() == "true"
93
+ )
94
+ self.DMM_ENABLED = yaml_analysis.get(
95
+ "delta_maintainability", os.getenv("DMM_ENABLED", "true").lower() == "true"
96
+ )
83
97
 
84
98
  # Temporal Filtering
85
99
  self.CREATED_AFTER = os.getenv("CREATED_AFTER")
@@ -102,8 +116,7 @@ class Config:
102
116
 
103
117
  # Output Configuration (YAML: output.directory)
104
118
  yaml_output = self._yaml_config.get("output", {})
105
- self.OUTPUT_DIR = Path(yaml_output.get("directory",
106
- os.getenv("OUTPUT_DIR", "./data")))
119
+ self.OUTPUT_DIR = Path(yaml_output.get("directory", os.getenv("OUTPUT_DIR", "./data")))
107
120
  self.OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
108
121
 
109
122
  # File Paths
@@ -119,25 +132,24 @@ class Config:
119
132
  yaml_urls = self._yaml_config.get("sources", {}).get("urls", [])
120
133
  env_urls = self._parse_repository_urls(os.getenv("REPOSITORY_URLS", ""))
121
134
  self.REPOSITORY_URLS: List[str] = yaml_urls if yaml_urls else env_urls
122
-
135
+
123
136
  # Clone path (YAML: extraction.clone_path)
124
- self.CLONE_PATH = Path(yaml_extraction.get("clone_path",
125
- os.getenv("CLONE_PATH", "/tmp/greenmining_repos")))
126
- self.CLEANUP_AFTER_ANALYSIS = (
127
- os.getenv("CLEANUP_AFTER_ANALYSIS", "true").lower() == "true"
137
+ self.CLONE_PATH = Path(
138
+ yaml_extraction.get("clone_path", os.getenv("CLONE_PATH", "/tmp/greenmining_repos"))
128
139
  )
140
+ self.CLEANUP_AFTER_ANALYSIS = os.getenv("CLEANUP_AFTER_ANALYSIS", "true").lower() == "true"
129
141
 
130
142
  # Energy Measurement (YAML: energy.*)
131
143
  yaml_energy = self._yaml_config.get("energy", {})
132
- self.ENERGY_ENABLED = yaml_energy.get("enabled",
133
- os.getenv("ENERGY_ENABLED", "false").lower() == "true")
134
- self.ENERGY_BACKEND = yaml_energy.get("backend",
135
- os.getenv("ENERGY_BACKEND", "rapl"))
136
- self.CARBON_TRACKING = yaml_energy.get("carbon_tracking",
137
- os.getenv("CARBON_TRACKING", "false").lower() == "true")
138
- self.COUNTRY_ISO = yaml_energy.get("country_iso",
139
- os.getenv("COUNTRY_ISO", "USA"))
140
-
144
+ self.ENERGY_ENABLED = yaml_energy.get(
145
+ "enabled", os.getenv("ENERGY_ENABLED", "false").lower() == "true"
146
+ )
147
+ self.ENERGY_BACKEND = yaml_energy.get("backend", os.getenv("ENERGY_BACKEND", "rapl"))
148
+ self.CARBON_TRACKING = yaml_energy.get(
149
+ "carbon_tracking", os.getenv("CARBON_TRACKING", "false").lower() == "true"
150
+ )
151
+ self.COUNTRY_ISO = yaml_energy.get("country_iso", os.getenv("COUNTRY_ISO", "USA"))
152
+
141
153
  # Power profiling (YAML: energy.power_profiling.*)
142
154
  yaml_power = yaml_energy.get("power_profiling", {})
143
155
  self.POWER_PROFILING_ENABLED = yaml_power.get("enabled", False)
@@ -0,0 +1,5 @@
1
+ # Web dashboard module for GreenMining visualization.
2
+
3
+ from .app import create_app, run_dashboard
4
+
5
+ __all__ = ["create_app", "run_dashboard"]
@@ -0,0 +1,200 @@
1
+ # Flask-based web dashboard for GreenMining analysis visualization.
2
+ # Provides interactive charts for repository analysis, pattern distribution,
3
+ # temporal trends, and energy consumption.
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+
11
+
12
+ def create_app(data_dir: str = "./data"):
13
+ # Create Flask application for the dashboard.
14
+ # Args:
15
+ # data_dir: Path to directory containing analysis JSON files
16
+ # Returns:
17
+ # Flask application instance
18
+ try:
19
+ from flask import Flask, render_template_string, jsonify, request
20
+ except ImportError:
21
+ raise ImportError("Flask is required for the dashboard. Install it with: pip install flask")
22
+
23
+ app = Flask(__name__)
24
+ data_path = Path(data_dir)
25
+
26
+ def _load_data(filename: str) -> Dict[str, Any]:
27
+ filepath = data_path / filename
28
+ if filepath.exists():
29
+ with open(filepath, encoding="utf-8") as f:
30
+ return json.load(f)
31
+ return {}
32
+
33
+ @app.route("/")
34
+ def index():
35
+ return render_template_string(DASHBOARD_HTML)
36
+
37
+ @app.route("/api/repositories")
38
+ def api_repositories():
39
+ data = _load_data("repositories.json")
40
+ return jsonify(data)
41
+
42
+ @app.route("/api/analysis")
43
+ def api_analysis():
44
+ data = _load_data("analysis_results.json")
45
+ return jsonify(data)
46
+
47
+ @app.route("/api/statistics")
48
+ def api_statistics():
49
+ data = _load_data("aggregated_statistics.json")
50
+ return jsonify(data)
51
+
52
+ @app.route("/api/energy")
53
+ def api_energy():
54
+ data = _load_data("energy_report.json")
55
+ return jsonify(data)
56
+
57
+ @app.route("/api/summary")
58
+ def api_summary():
59
+ # Build summary from available data
60
+ repos = _load_data("repositories.json")
61
+ analysis = _load_data("analysis_results.json")
62
+
63
+ repo_count = 0
64
+ if isinstance(repos, list):
65
+ repo_count = len(repos)
66
+ elif isinstance(repos, dict):
67
+ repo_count = repos.get("total_repositories", len(repos.get("repositories", [])))
68
+
69
+ commit_count = 0
70
+ green_count = 0
71
+ if isinstance(analysis, list):
72
+ commit_count = len(analysis)
73
+ green_count = sum(1 for a in analysis if a.get("green_aware"))
74
+ elif isinstance(analysis, dict):
75
+ results = analysis.get("results", [])
76
+ for r in results:
77
+ commits = r.get("commits", [])
78
+ commit_count += len(commits)
79
+ green_count += sum(1 for c in commits if c.get("green_aware"))
80
+
81
+ green_rate = (green_count / commit_count * 100) if commit_count > 0 else 0
82
+
83
+ return jsonify(
84
+ {
85
+ "repositories": repo_count,
86
+ "commits_analyzed": commit_count,
87
+ "green_commits": green_count,
88
+ "green_rate": round(green_rate, 1),
89
+ }
90
+ )
91
+
92
+ return app
93
+
94
+
95
+ def run_dashboard(data_dir: str = "./data", host: str = "127.0.0.1", port: int = 5000):
96
+ # Run the dashboard server.
97
+ # Args:
98
+ # data_dir: Path to analysis data directory
99
+ # host: Host to bind to
100
+ # port: Port to bind to
101
+ app = create_app(data_dir)
102
+ print(f"GreenMining Dashboard running at http://{host}:{port}")
103
+ print(f"Data directory: {data_dir}")
104
+ app.run(host=host, port=port, debug=False)
105
+
106
+
107
+ # Dashboard HTML template with embedded JS (no external dependencies)
108
+ DASHBOARD_HTML = """
109
+ <!DOCTYPE html>
110
+ <html lang="en">
111
+ <head>
112
+ <meta charset="UTF-8">
113
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
114
+ <title>GreenMining Dashboard</title>
115
+ <style>
116
+ * { margin: 0; padding: 0; box-sizing: border-box; }
117
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
118
+ background: #f5f7fa; color: #333; }
119
+ .header { background: #1a472a; color: white; padding: 20px 40px; }
120
+ .header h1 { font-size: 24px; font-weight: 600; }
121
+ .header p { font-size: 14px; opacity: 0.8; margin-top: 4px; }
122
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
123
+ .grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
124
+ .card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
125
+ .card h3 { font-size: 13px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
126
+ .card .value { font-size: 32px; font-weight: 700; color: #1a472a; margin-top: 8px; }
127
+ .card .subtitle { font-size: 12px; color: #999; margin-top: 4px; }
128
+ .section { margin-bottom: 24px; }
129
+ .section h2 { font-size: 18px; margin-bottom: 12px; color: #1a472a; }
130
+ table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px;
131
+ overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
132
+ th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #eee; }
133
+ th { background: #f8f9fa; font-size: 12px; text-transform: uppercase; color: #666; }
134
+ td { font-size: 14px; }
135
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; }
136
+ .badge-green { background: #d4edda; color: #155724; }
137
+ .badge-gray { background: #e9ecef; color: #495057; }
138
+ .bar { height: 8px; background: #e9ecef; border-radius: 4px; overflow: hidden; }
139
+ .bar-fill { height: 100%; background: #1a472a; border-radius: 4px;
140
+ transition: width 0.5s ease; }
141
+ .loading { text-align: center; padding: 40px; color: #999; }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <div class="header">
146
+ <h1>GreenMining Dashboard</h1>
147
+ <p>Mining Software Repositories for Green IT Research</p>
148
+ </div>
149
+ <div class="container">
150
+ <div class="grid" id="summary-cards">
151
+ <div class="card"><h3>Repositories</h3><div class="value" id="repo-count">-</div></div>
152
+ <div class="card"><h3>Commits Analyzed</h3><div class="value" id="commit-count">-</div></div>
153
+ <div class="card"><h3>Green Commits</h3><div class="value" id="green-count">-</div></div>
154
+ <div class="card"><h3>Green Rate</h3><div class="value" id="green-rate">-</div></div>
155
+ </div>
156
+ <div class="section">
157
+ <h2>Repositories</h2>
158
+ <div id="repo-table"><div class="loading">Loading data...</div></div>
159
+ </div>
160
+ </div>
161
+ <script>
162
+ async function loadDashboard() {
163
+ try {
164
+ const summary = await fetch('/api/summary').then(r => r.json());
165
+ document.getElementById('repo-count').textContent = summary.repositories;
166
+ document.getElementById('commit-count').textContent = summary.commits_analyzed.toLocaleString();
167
+ document.getElementById('green-count').textContent = summary.green_commits.toLocaleString();
168
+ document.getElementById('green-rate').textContent = summary.green_rate + '%';
169
+ } catch(e) {
170
+ console.log('Summary not available:', e);
171
+ }
172
+
173
+ try {
174
+ const repos = await fetch('/api/repositories').then(r => r.json());
175
+ const list = repos.repositories || (Array.isArray(repos) ? repos : []);
176
+ if (list.length > 0) {
177
+ let html = '<table><thead><tr><th>Repository</th><th>Language</th>' +
178
+ '<th>Stars</th><th>Description</th></tr></thead><tbody>';
179
+ list.slice(0, 50).forEach(r => {
180
+ html += '<tr><td><strong>' + (r.full_name || r.name) + '</strong></td>' +
181
+ '<td><span class="badge badge-green">' + (r.language || '-') + '</span></td>' +
182
+ '<td>' + (r.stars || 0).toLocaleString() + '</td>' +
183
+ '<td>' + (r.description || '-').substring(0, 80) + '</td></tr>';
184
+ });
185
+ html += '</tbody></table>';
186
+ document.getElementById('repo-table').innerHTML = html;
187
+ } else {
188
+ document.getElementById('repo-table').innerHTML =
189
+ '<div class="card">No repository data found. Run an analysis first.</div>';
190
+ }
191
+ } catch(e) {
192
+ document.getElementById('repo-table').innerHTML =
193
+ '<div class="card">Run an analysis to populate the dashboard.</div>';
194
+ }
195
+ }
196
+ loadDashboard();
197
+ </script>
198
+ </body>
199
+ </html>
200
+ """
@@ -1,13 +1,20 @@
1
1
  # Energy measurement module for GreenMining.
2
2
 
3
- from .base import EnergyMeter, EnergyMetrics, EnergyBackend
3
+ from .base import EnergyMeter, EnergyMetrics, EnergyBackend, CommitEnergyProfile, get_energy_meter
4
4
  from .rapl import RAPLEnergyMeter
5
5
  from .codecarbon_meter import CodeCarbonMeter
6
+ from .cpu_meter import CPUEnergyMeter
7
+ from .carbon_reporter import CarbonReporter, CarbonReport
6
8
 
7
9
  __all__ = [
8
10
  "EnergyMeter",
9
11
  "EnergyMetrics",
10
12
  "EnergyBackend",
13
+ "CommitEnergyProfile",
14
+ "get_energy_meter",
11
15
  "RAPLEnergyMeter",
12
16
  "CodeCarbonMeter",
17
+ "CPUEnergyMeter",
18
+ "CarbonReporter",
19
+ "CarbonReport",
13
20
  ]