Skip to content

Commit d1111f9

Browse files
committed
Enhance dashboard report generation: add Markdown and HTML export options, improve button usability, and refactor code for clarity
1 parent 3f67253 commit d1111f9

4 files changed

Lines changed: 317 additions & 195 deletions

File tree

src/portfolio_auditor/dashboard/app.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
fetch_live_repo_sync_result,
2323
should_refresh_audit,
2424
)
25+
from portfolio_auditor.exports import MarkdownExporter
2526
from portfolio_auditor.settings import get_settings, reset_settings_cache
2627

2728

@@ -259,7 +260,7 @@ def main() -> None:
259260
"Use refresh when repositories changed recently or when you want the latest "
260261
"local scan evidence."
261262
)
262-
if st.button("Run fresh audit now", type="primary", width='stretch'):
263+
if st.button("Run fresh audit now", type="primary", use_container_width=True):
263264
with st.spinner(
264265
"Refreshing portfolio artifacts from GitHub. This can take a while for "
265266
"larger portfolios."
@@ -274,6 +275,14 @@ def main() -> None:
274275

275276
try:
276277
data = load_dashboard_data(owner)
278+
report_md = MarkdownExporter.from_artifacts_dir(
279+
data.base_dir,
280+
output_format="markdown",
281+
)
282+
report_html = MarkdownExporter.from_artifacts_dir(
283+
data.base_dir,
284+
output_format="html",
285+
)
277286
except DashboardDataError as exc:
278287
st.error(str(exc))
279288
st.info(
@@ -292,6 +301,7 @@ def main() -> None:
292301

293302
ranking_path = data.base_dir / "ranking.json"
294303
mtime = ranking_path.stat().st_mtime if ranking_path.exists() else None
304+
295305
with st.sidebar:
296306
_render_staleness_indicator(mtime)
297307
st.markdown("---")
@@ -300,7 +310,7 @@ def main() -> None:
300310
refresh_decision = should_refresh_audit(sync_result)
301311
if refresh_decision.should_refresh and st.button(
302312
"Refresh audit to sync GitHub changes",
303-
width='stretch',
313+
use_container_width=True,
304314
):
305315
with st.spinner(
306316
"Refreshing portfolio artifacts to include the latest GitHub changes."
@@ -315,6 +325,7 @@ def main() -> None:
315325
st.info(f"GitHub sync check unavailable: {sync_error}")
316326

317327
repo_options = data.repo_df["repo_name"].tolist()
328+
318329
with st.sidebar:
319330
selected_repo = st.selectbox("Repository detail", options=repo_options)
320331
st.markdown("---")
@@ -347,7 +358,7 @@ def main() -> None:
347358
)
348359
if data.comparison_summary.get("snapshot_created_at_utc"):
349360
st.caption(
350-
f"Compared with snapshot: "
361+
"Compared with snapshot: "
351362
f"{data.comparison_summary['snapshot_created_at_utc']}"
352363
)
353364

@@ -362,6 +373,23 @@ def main() -> None:
362373
f"ROI {best_action['roi']:.2f}"
363374
)
364375

376+
st.markdown("---")
377+
st.markdown("### Export report")
378+
st.download_button(
379+
label="Download Markdown report",
380+
data=report_md,
381+
file_name=f"{owner}-portfolio-report.md",
382+
mime="text/markdown",
383+
use_container_width=True,
384+
)
385+
st.download_button(
386+
label="Download HTML report",
387+
data=report_html,
388+
file_name=f"{owner}-portfolio-report.html",
389+
mime="text/html",
390+
use_container_width=True,
391+
)
392+
365393
tabs = st.tabs(
366394
[
367395
"Overview",
@@ -393,4 +421,4 @@ def main() -> None:
393421

394422

395423
if __name__ == "__main__":
396-
main()
424+
main()

src/portfolio_auditor/dashboard/optimizer.py

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -40,45 +40,39 @@
4040
},
4141
"Write a complete README.": {
4242
"penalty_codes": {"README_MISSING", "README_TOO_SHORT", "USAGE_MISSING"},
43-
"fallback_points": 0.0,
43+
"fallback_points": 6.0,
4444
"effort_units": 2.0,
4545
"category": "documentation",
4646
},
47-
"Add and apply a proper .gitignore.": {
48-
"penalty_codes": {"PYCACHE_COMMITTED", "BUILD_ARTIFACTS_COMMITTED"},
49-
"fallback_points": 0.0,
50-
"effort_units": 1.0,
51-
"category": "hygiene",
52-
},
53-
"Delete committed __pycache__ directories.": {
54-
"penalty_codes": {"PYCACHE_COMMITTED"},
55-
"fallback_points": 0.0,
47+
"Add a concise GitHub description.": {
48+
"penalty_codes": {"DESCRIPTION_MISSING"},
49+
"fallback_points": 1.0,
5650
"effort_units": 0.5,
57-
"category": "hygiene",
51+
"category": "documentation",
5852
},
59-
"Remove generated build/cache artifacts.": {
60-
"penalty_codes": {"BUILD_ARTIFACTS_COMMITTED"},
61-
"fallback_points": 0.0,
62-
"effort_units": 0.75,
63-
"category": "hygiene",
53+
"Expose a demo or homepage link when relevant.": {
54+
"penalty_codes": {"HOMEPAGE_MISSING", "DEMO_MISSING"},
55+
"fallback_points": 2.0,
56+
"effort_units": 0.5,
57+
"category": "portfolio_relevance",
6458
},
65-
"Review large committed files and keep only necessary assets.": {
66-
"penalty_codes": {"OVERSIZED_FILES"},
67-
"fallback_points": 0.0,
59+
"Clarify the portfolio positioning and business value.": {
60+
"penalty_codes": {"PORTFOLIO_VALUE_UNCLEAR"},
61+
"fallback_points": 3.0,
6862
"effort_units": 1.5,
69-
"category": "hygiene",
63+
"category": "portfolio_relevance",
7064
},
71-
"Expose a demo or homepage link when relevant.": {
72-
"penalty_codes": set(),
73-
"fallback_points": 1.5,
74-
"effort_units": 1.0,
75-
"category": "portfolio_signal",
65+
"Strengthen the technical depth of the implementation.": {
66+
"penalty_codes": {"TECHNICAL_DEPTH_LIMITED"},
67+
"fallback_points": 4.0,
68+
"effort_units": 3.0,
69+
"category": "technical_depth",
7670
},
77-
"Add a concise GitHub description.": {
78-
"penalty_codes": set(),
79-
"fallback_points": 1.0,
80-
"effort_units": 0.5,
81-
"category": "portfolio_signal",
71+
"Improve maintainability and code cleanliness.": {
72+
"penalty_codes": {"MAINTAINABILITY_WEAK"},
73+
"fallback_points": 3.0,
74+
"effort_units": 2.0,
75+
"category": "maintainability",
8276
},
8377
}
8478

@@ -103,24 +97,42 @@ def estimate_action_impact(
10397
action_text,
10498
{
10599
"penalty_codes": set(),
106-
"fallback_points": 0.75,
107-
"effort_units": 1.5,
100+
"fallback_points": 2.0,
101+
"effort_units": 2.0,
108102
"category": "general",
109103
},
110104
)
111105

112-
penalty_points = repo_penalty_points(score_entry)
113-
matched_codes = sorted(code for code in rule["penalty_codes"] if penalty_points.get(code, 0.0) > 0)
114-
estimated_points = sum(penalty_points.get(code, 0.0) for code in matched_codes)
106+
penalty_index = repo_penalty_points(score_entry)
115107

116-
if estimated_points <= 0 and action_text == "Expose a demo or homepage link when relevant.":
117-
homepage = str(repo_row.get("homepage") or "").strip()
118-
if not homepage:
108+
review_matched_codes = [
109+
str(code).strip()
110+
for code in (review.get("matched_penalty_codes", []) or [])
111+
if str(code).strip()
112+
]
113+
114+
if review_matched_codes:
115+
matched_codes = [code for code in review_matched_codes if code in rule["penalty_codes"]]
116+
else:
117+
matched_codes = [code for code in penalty_index if code in rule["penalty_codes"]]
118+
119+
estimated_points = round(sum(penalty_index.get(code, 0.0) for code in matched_codes), 2)
120+
121+
if estimated_points <= 0 and action_text == "Write a complete README.":
122+
readme_length = int(repo_row.get("readme_length", 0) or 0)
123+
if readme_length == 0:
119124
estimated_points = float(rule["fallback_points"])
125+
120126
elif estimated_points <= 0 and action_text == "Add a concise GitHub description.":
121127
description = str(repo_row.get("description") or "").strip()
122128
if not description:
123129
estimated_points = float(rule["fallback_points"])
130+
131+
elif estimated_points <= 0 and action_text == "Expose a demo or homepage link when relevant.":
132+
homepage = str(repo_row.get("homepage") or "").strip()
133+
if not homepage:
134+
estimated_points = float(rule["fallback_points"])
135+
124136
elif estimated_points <= 0 and action_text not in ACTION_IMPACT_RULES:
125137
estimated_points = float(rule["fallback_points"])
126138

@@ -187,7 +199,8 @@ def derive_repo_optimizer_fields(
187199

188200

189201
def build_next_actions(
190-
df: pd.DataFrame, review_index: dict[str, dict[str, Any]]
202+
df: pd.DataFrame,
203+
review_index: dict[str, dict[str, Any]],
191204
) -> list[dict[str, Any]]:
192205
counter: Counter[str] = Counter()
193206
action_rows: dict[str, list[dict[str, Any]]] = {}
@@ -210,12 +223,14 @@ def build_next_actions(
210223
"estimated_score_lift": float(opportunity.get("estimated_score_lift", 0.0)),
211224
"effort_units": float(opportunity.get("effort_units", 0.0)),
212225
"roi": float(opportunity.get("roi", 0.0)),
213-
"matched_penalty_codes": list(opportunity.get("matched_penalty_codes", [])),
226+
"matched_penalty_codes": list(
227+
opportunity.get("matched_penalty_codes", [])
228+
),
214229
}
215230
)
216231

217232
ranked_actions: list[dict[str, Any]] = []
218-
for action_text, _count in counter.most_common(12):
233+
for action_text in counter:
219234
repos = sorted(
220235
action_rows[action_text],
221236
key=lambda item: (
@@ -241,7 +256,16 @@ def build_next_actions(
241256
"repos": repos[:6],
242257
}
243258
)
244-
return ranked_actions
259+
260+
return sorted(
261+
ranked_actions,
262+
key=lambda item: (
263+
-item["roi"],
264+
-item["estimated_total_score_lift"],
265+
-item["affected_repo_count"],
266+
item["action"],
267+
),
268+
)[:12]
245269

246270

247271
def simulate_portfolio(
@@ -263,12 +287,15 @@ def simulate_portfolio(
263287
def _project_for_scope(scope_df: pd.DataFrame, top_n: int) -> float:
264288
if scope_df.empty:
265289
return 0.0
290+
266291
repo_lifts: dict[str, float] = {row["repo_name"]: 0.0 for _, row in scope_df.iterrows()}
292+
267293
for action in prioritized_actions[:top_n]:
268294
for repo in action["repos"]:
269295
repo_name = str(repo.get("repo_name"))
270296
if repo_name not in repo_lifts:
271297
continue
298+
272299
current_score = float(
273300
scope_df.loc[scope_df["repo_name"] == repo_name, "global_score"].iloc[0]
274301
)
@@ -278,6 +305,7 @@ def _project_for_scope(scope_df: pd.DataFrame, top_n: int) -> float:
278305
remaining = max(0.0, ceiling - current_score - repo_lifts[repo_name])
279306
lift = min(float(repo.get("estimated_score_lift", 0.0)), remaining)
280307
repo_lifts[repo_name] += lift
308+
281309
projected_scores = [
282310
min(100.0, float(row["global_score"]) + repo_lifts.get(str(row["repo_name"]), 0.0))
283311
for _, row in scope_df.iterrows()
@@ -298,4 +326,4 @@ def _project_for_scope(scope_df: pd.DataFrame, top_n: int) -> float:
298326
"selected_scope_current": selected_scope_quality,
299327
"selected_scope_after_top_3": selected_scope_after_top_three,
300328
"top_actions": prioritized_actions[:3],
301-
}
329+
}

0 commit comments

Comments
 (0)