blob: 6da5f5f3286b473835a9b135444f5b9ce78da40d [file]
from __future__ import annotations
import os
import re
import subprocess
from pathlib import Path
from typing import Optional
import pandas as pd
import streamlit as st
import altair as alt
# ============================
# Git helpers (use THIS repo)
# ============================
def run_git(repo_dir: Path, args: list[str]) -> str:
p = subprocess.run(
["git", "-C", str(repo_dir), *args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
env={**dict(os.environ), "GIT_TERMINAL_PROMPT": "0"},
)
if p.returncode != 0:
raise RuntimeError(p.stderr.strip() or "git command failed")
return p.stdout
def resolve_repo_root() -> Path:
"""
Use the dashboard repo itself as the git root.
This works locally and on Streamlit Community Cloud.
"""
cwd = Path.cwd().resolve()
try:
p = subprocess.run(
["git", "-C", str(cwd), "rev-parse", "--show-toplevel"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
return Path(p.stdout.strip())
except subprocess.CalledProcessError as e:
raise RuntimeError(
"This app must be run from a git checkout.\n"
f"cwd={cwd}\n{e.stderr}"
)
_RE_LS_TREE_BLOB = re.compile(r"^\d+\s+blob\s+([0-9a-f]{40})\t(.+)$")
def report_set_commits(repo_dir: Path, reports_dir: str, max_sets: int) -> list[str]:
out = run_git(repo_dir, ["log", f"--max-count={max_sets}", "--pretty=format:%H", "--", reports_dir])
return [x.strip() for x in out.splitlines() if x.strip()]
def report_blobs_at_commit(repo_dir: Path, commit: str, reports_dir: str) -> list[tuple[str, str]]:
out = run_git(repo_dir, ["ls-tree", "-r", commit, reports_dir])
pairs: list[tuple[str, str]] = []
for line in out.splitlines():
m = _RE_LS_TREE_BLOB.match(line.strip())
if not m:
continue
oid, path = m.group(1), m.group(2)
if path.endswith(".md"):
pairs.append((path, oid))
return pairs
def cat_blobs(repo_dir: Path, oids: list[str]) -> dict[str, str]:
if not oids:
return {}
proc = subprocess.Popen(
["git", "-C", str(repo_dir), "cat-file", "--batch"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert proc.stdin and proc.stdout
proc.stdin.write(("\n".join(oids) + "\n").encode("utf-8"))
proc.stdin.flush()
out: dict[str, str] = {}
for _ in oids:
header_b = proc.stdout.readline()
if not header_b:
break
header = header_b.decode("utf-8", errors="replace").strip()
parts = header.split()
if len(parts) < 3:
continue
got_oid, typ, size_s = parts[0], parts[1], parts[2]
size = int(size_s) if size_s.isdigit() else 0
content = proc.stdout.read(size)
proc.stdout.read(1) # trailing newline
if typ == "blob":
out[got_oid] = content.decode("utf-8", errors="replace")
proc.stdin.close()
proc.wait(timeout=120)
return out
# ============================
# Parsing (based on actual report format)
# ============================
RE_GENERATED_ON = re.compile(r"_Generated on\s+(\d{4}-\d{2}-\d{2})_", re.IGNORECASE)
RE_WINDOW_HEADER = re.compile(r"^###\s+(3m|6m|12m)\b", re.MULTILINE)
RE_RELEASES = re.compile(r"Releases .*?:\s*(\d+)\s*\|\s*Median gap \(days\):\s*([0-9.]+|—)")
RE_CONTRIB = re.compile(r"New contributors:\s*(\d+)\s*\|\s*Unique committers:\s*(\d+)\s*\|\s*Commits:\s*(\d+)")
RE_ISSUES = re.compile(r"Issues:\s*opened\s*(\d+)\s*/\s*closed\s*(\d+)")
RE_PRS = re.compile(r"PRs:\s*opened\s*(\d+)\s*/\s*merged\s*(\d+)\s*\|\s*Median merge time \(days\):\s*([0-9.]+|—)")
RE_REVIEWS = re.compile(
r"Reviews \(sampled\):.*?median reviewers/PR\s*([0-9.]+|—).*?"
r"reviewer diversity \(eff\.\#\)\s*([0-9.]+|—).*?"
r"PR author diversity \(eff\.\#\)\s*([0-9.]+|—).*?"
r"unique reviewers\s*(\d+|—),\s*unique authors\s*(\d+|—)"
)
RE_BUS = re.compile(r"Bus factor proxy \(50%\s*/\s*75%\):\s*(\d+|—)\s*/\s*(\d+|—)")
RE_REPORTS = re.compile(r"Incubator reports:\s*(\d+|—)\s*\|\s*Avg mentor sign-offs:\s*([0-9.]+|—)")
RE_MAIL = re.compile(r"Mailing lists:\s*dev messages\s*(\d+|—),\s*dev unique posters\s*(\d+|—)")
def _to_int(s: Optional[str]) -> Optional[int]:
if s is None or s == "—":
return None
return int(s)
def _to_float(s: Optional[str]) -> Optional[float]:
if s is None or s == "—":
return None
return float(s)
def strip_md(line: str) -> str:
return line.replace("**", "").strip()
def parse_window_details_block(bullet_lines: list[str]) -> dict:
row: dict = {}
for raw in bullet_lines:
line = strip_md(raw)
line = line[2:].strip() if line.startswith("- ") else line
if line.startswith("Releases "):
m = RE_RELEASES.search(line)
if m:
row["releases"] = _to_int(m.group(1))
row["median_gap_days"] = _to_float(m.group(2))
elif line.startswith("New contributors:"):
m = RE_CONTRIB.search(line)
if m:
row["new_contributors"] = _to_int(m.group(1))
row["unique_committers"] = _to_int(m.group(2))
row["commits"] = _to_int(m.group(3))
elif line.startswith("Issues:"):
m = RE_ISSUES.search(line)
if m:
row["issues_opened"] = _to_int(m.group(1))
row["issues_closed"] = _to_int(m.group(2))
elif line.startswith("PRs:"):
m = RE_PRS.search(line)
if m:
row["prs_opened"] = _to_int(m.group(1))
row["prs_merged"] = _to_int(m.group(2))
row["median_merge_days"] = _to_float(m.group(3))
elif line.startswith("Reviews (sampled):"):
m = RE_REVIEWS.search(line)
if m:
row["median_reviewers_per_pr"] = _to_float(m.group(1))
row["reviewer_div_eff"] = _to_float(m.group(2))
row["pr_author_div_eff"] = _to_float(m.group(3))
row["unique_reviewers"] = _to_int(m.group(4))
row["unique_authors"] = _to_int(m.group(5))
elif line.startswith("Bus factor proxy"):
m = RE_BUS.search(line)
if m:
row["bus50"] = _to_int(m.group(1))
row["bus75"] = _to_int(m.group(2))
elif line.startswith("Incubator reports:"):
m = RE_REPORTS.search(line)
if m:
row["reports_count"] = _to_int(m.group(1))
row["avg_mentor_signoffs"] = _to_float(m.group(2))
elif line.startswith("Mailing lists:"):
m = RE_MAIL.search(line)
if m:
row["dev_msgs"] = _to_int(m.group(1))
row["dev_unique_posters"] = _to_int(m.group(2))
return row
def parse_report(text: str, podling_stem: str) -> list[dict]:
m = RE_GENERATED_ON.search(text)
if not m:
return []
snapshot_date = m.group(1)
wd_idx = text.find("## Window Details")
if wd_idx == -1:
return []
wd = text[wd_idx:]
matches = list(RE_WINDOW_HEADER.finditer(wd))
out: list[dict] = []
for i, mm in enumerate(matches):
window = mm.group(1)
start = mm.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(wd)
block = wd[start:end]
bullet_lines = [ln for ln in block.splitlines() if ln.strip().startswith("- ")]
metrics = parse_window_details_block(bullet_lines)
out.append(
{
"podling_key": podling_stem.casefold(), # merge Hamalton/hamalton/etc
"podling_display": podling_stem, # keep original for display; latest wins
"snapshot_date": snapshot_date,
"window": window,
**metrics,
}
)
return out
# ============================
# Dataset build (git history)
# ============================
@st.cache_data(show_spinner=True)
def build_dataset(repo_root: str, reports_dir: str, max_commits: int) -> pd.DataFrame:
repo_dir = Path(repo_root)
rows: list[dict] = []
commits = report_set_commits(repo_dir, reports_dir, max_sets=max_commits)
for commit in commits:
pairs = report_blobs_at_commit(repo_dir, commit, reports_dir)
if not pairs:
continue
oids = [oid for (_path, oid) in pairs]
blob_text = cat_blobs(repo_dir, oids)
for path, oid in pairs:
text = blob_text.get(oid)
if not text:
continue
podling_stem = Path(path).stem
for r in parse_report(text, podling_stem):
r["commit"] = commit
r["path"] = path
rows.append(r)
df = pd.DataFrame(rows)
if df.empty:
return df
df["snapshot_date"] = pd.to_datetime(df["snapshot_date"], errors="coerce")
df = df.dropna(subset=["snapshot_date"])
# de-dupe
df = df.sort_values(["podling_key", "window", "snapshot_date", "commit"])
df = df.drop_duplicates(["podling_key", "window", "snapshot_date"], keep="last")
# ensure one display name per key: latest snapshot wins
latest_name = (
df.sort_values(["podling_key", "snapshot_date", "commit"])
.groupby("podling_key", as_index=False)
.tail(1)[["podling_key", "podling_display"]]
)
df = df.drop(columns=["podling_display"]).merge(latest_name, on="podling_key", how="left")
return df.reset_index(drop=True)
# ============================
# Latest-only combined snapshot (STRICT)
# - availability and signals come ONLY from the latest snapshot per podling_key
# - windows count ONLY if present in that latest snapshot (no inference)
# ============================
def latest_combo(df: pd.DataFrame) -> pd.DataFrame:
if df.empty:
return df
# latest snapshot_date per podling_key
latest_dates = df.groupby("podling_key", as_index=False)["snapshot_date"].max()
latest = df.merge(latest_dates, on=["podling_key", "snapshot_date"], how="inner")
# if multiple commits same day, keep latest commit row per (key, window)
latest = latest.sort_values(["podling_key", "window", "snapshot_date", "commit"]).groupby(
["podling_key", "window"], as_index=False
).tail(1)
keep = ["podling_key", "podling_display", "window", "snapshot_date"]
metrics = [c for c in latest.columns if c not in (keep + ["commit", "path"])]
wide = latest.pivot(index="podling_key", columns="window", values=metrics)
wide.columns = [f"{m}_{w}" for (m, w) in wide.columns]
wide = wide.reset_index()
dates = latest.pivot(index="podling_key", columns="window", values="snapshot_date")
dates.columns = [f"snapshot_date_{w}" for w in dates.columns]
dates = dates.reset_index()
names = latest.groupby("podling_key", as_index=False).tail(1)[["podling_key", "podling_display"]]
out = wide.merge(dates, on="podling_key", how="left").merge(names, on="podling_key", how="left")
# add single "latest_snapshot_date" (for info / debugging if needed)
out["latest_snapshot_date"] = latest.groupby("podling_key")["snapshot_date"].max().reindex(out["podling_key"]).values
return out
# ============================
# Signals + commentary (STRICT latest-only windows)
# ============================
PRIMARY = "primary"
SECONDARY = "secondary"
FYI = "fyi"
def _val(row: pd.Series, key: str) -> Optional[float]:
v = row.get(key)
if v is None:
return None
if isinstance(v, float) and pd.isna(v):
return None
if isinstance(v, (int, float)):
return float(v)
return None
def _has_window(row: pd.Series, w: str) -> bool:
d = row.get(f"snapshot_date_{w}")
return d is not None and not pd.isna(d)
def window_availability_messages(row: pd.Series) -> list[str]:
# STRICT: only based on latest snapshot (presence of ### window section in that latest file)
msgs: list[str] = []
for w in ["12m", "6m", "3m"]:
msgs.append(f"- **{w}**: {'available' if _has_window(row, w) else 'not available'}")
return msgs
def build_signals_latest_only(row: pd.Series) -> list[dict]:
sig: list[dict] = []
def add(level: str, text: str):
sig.append({"level": level, "text": text})
has3 = _has_window(row, "3m")
has6 = _has_window(row, "6m")
has12 = _has_window(row, "12m")
# pull values ONLY if the window exists
r6 = _val(row, "releases_6m") if has6 else None
r12 = _val(row, "releases_12m") if has12 else None
p3 = _val(row, "dev_unique_posters_3m") if has3 else None
p12 = _val(row, "dev_unique_posters_12m") if has12 else None
m3 = _val(row, "dev_msgs_3m") if has3 else None
m12 = _val(row, "dev_msgs_12m") if has12 else None
bus3 = _val(row, "bus50_3m") if has3 else None
committers12 = _val(row, "unique_committers_12m") if has12 else None
merge3 = _val(row, "median_merge_days_3m") if has3 else None
merge12 = _val(row, "median_merge_days_12m") if has12 else None
prs3 = _val(row, "prs_merged_3m") if has3 else None
revdiv3 = _val(row, "reviewer_div_eff_3m") if has3 else None
authdiv3 = _val(row, "pr_author_div_eff_3m") if has3 else None
commits3 = _val(row, "commits_3m") if has3 else None
# Releases checks: only when the relevant windows exist in the LATEST report
if has12 and r12 is not None and r12 == 0:
add(PRIMARY, "No releases in 12m — likely needs mentor/IPMC attention (blockers/ownership/process).")
elif has6 and has12 and r6 is not None and r6 == 0 and (r12 is not None and r12 > 0):
add(SECONDARY, "No releases in 6m but releases exist in 12m — check-in: planned pause or emerging blockers?")
# Participation vs baseline: only compare if BOTH windows exist in the LATEST report
if has3 and has12 and p3 is not None and p12 is not None and p12 > 0:
ratio = p3 / p12
if p12 >= 15 and p3 <= 6 and ratio <= 0.25:
add(
SECONDARY,
"Recent dev@ participation (3m) is materially lower than the 12m baseline — confirm whether discussion moved channels or participation narrowed."
)
if has3 and has12 and m3 is not None and m12 is not None and m12 > 0:
drop = (m12 - m3) / m12
if m3 <= 20 and drop >= 0.50:
add(
FYI,
"Recent dev@ message volume (3m) is lower than the 12m baseline — interpret alongside other indicators (releases/review activity)."
)
# Bus factor: needs 3m and 12m in the LATEST report
if has3 and has12 and bus3 is not None and bus3 <= 2 and committers12 is not None and committers12 <= 10:
add(SECONDARY, "Contribution concentration signal (bus50 ≤ 2) with small 12m committer base — check dominance risk.")
# PR throughput concentration: only needs 3m
if has3 and prs3 is not None and prs3 >= 50:
if revdiv3 is not None and revdiv3 <= 3.0:
add(SECONDARY, "High PR throughput but low reviewer diversity (3m) — are reviews concentrated?")
if authdiv3 is not None and authdiv3 <= 3.0:
add(SECONDARY, "Activity concentrated among few PR authors (3m) — is funnel broadening?")
# Merge time baseline comparison: only if BOTH windows exist in the LATEST report
if has3 and has12 and merge3 is not None and merge12 is not None and merge12 > 0:
ch = (merge3 - merge12) / merge12
if ch >= 0.50 and merge3 >= 7:
add(SECONDARY, "3m PR merge time much slower than 12m baseline — backlog/review capacity/process change?")
elif ch >= 0.25 and merge3 >= 7:
add(FYI, "3m PR merge time trending slower than 12m baseline — FYI (watch next snapshot).")
# Commit activity vs participation: only needs 3m
if has3 and commits3 is not None and commits3 >= 100 and (p3 is not None and p3 <= 5):
add(FYI, "High commit activity with low dev@ participation (3m) — FYI: check on-list socialisation norms.")
return sig
def signals_summary(sig: list[dict]) -> str:
if not sig:
return "—"
order = {PRIMARY: 0, SECONDARY: 1, FYI: 2}
sig = sorted(sig, key=lambda x: order.get(x["level"], 9))
return " • ".join([f"[{s['level']}] {s['text']}" for s in sig])
def draft_commentary_from_latest(pod_display: str, row: pd.Series) -> str:
sigs = row.get("signals") or []
order = {PRIMARY: 0, SECONDARY: 1, FYI: 2}
sigs = sorted(sigs, key=lambda x: order.get(x["level"], 9))
primary = [s for s in sigs if s["level"] == PRIMARY]
secondary = [s for s in sigs if s["level"] == SECONDARY]
fyi = [s for s in sigs if s["level"] == FYI]
lines: list[str] = []
lines.append(f"### {pod_display} — health cross-check summary")
lines.append("")
lines.append("**Data availability:**")
lines.extend(window_availability_messages(row))
lines.append("")
lines.append("**Indicators:**")
if not (primary or secondary or fyi):
lines.append("- —")
return "\n".join(lines)
if primary:
lines.append("- **Primary:**")
for s in primary:
lines.append(f" - {s['text']}")
if secondary:
lines.append("- **Secondary:**")
for s in secondary:
lines.append(f" - {s['text']}")
if fyi:
lines.append("- **FYI:**")
for s in fyi:
lines.append(f" - {s['text']}")
return "\n".join(lines)
# ============================
# Charts
# ============================
def altair_metric_chart(pod_df: pd.DataFrame, metric: str, title: str):
if metric not in pod_df.columns:
return
d = pod_df[["snapshot_date", "window", metric]].dropna().copy()
if d.empty:
return
c = (
alt.Chart(d)
.mark_line(point=True)
.encode(
x=alt.X("snapshot_date:T", title="Snapshot date"),
y=alt.Y(f"{metric}:Q", title=metric),
color=alt.Color(
"window:N",
title=None,
legend=alt.Legend(orient="bottom", direction="horizontal"),
),
tooltip=[
alt.Tooltip("snapshot_date:T", title="Snapshot"),
alt.Tooltip("window:N", title="Window"),
alt.Tooltip(f"{metric}:Q", title=metric),
],
)
.properties(title=title)
)
st.altair_chart(c, use_container_width=True)
def charts_panel(pod_df: pd.DataFrame):
c1, c2, c3 = st.columns(3)
with c1:
altair_metric_chart(pod_df, "commits", "Commits (3m/6m/12m)")
with c2:
altair_metric_chart(pod_df, "prs_merged", "PRs merged (3m/6m/12m)")
with c3:
altair_metric_chart(pod_df, "dev_unique_posters", "Dev unique posters (3m/6m/12m)")
c4, c5, c6 = st.columns(3)
with c4:
altair_metric_chart(pod_df, "releases", "Releases (3m/6m/12m)")
with c5:
altair_metric_chart(pod_df, "median_merge_days", "Median PR merge time (days) (3m/6m/12m)")
with c6:
altair_metric_chart(pod_df, "bus50", "Bus factor proxy 50% (3m/6m/12m)")
c7, c8, c9 = st.columns(3)
with c7:
altair_metric_chart(pod_df, "issues_opened", "Issues opened (3m/6m/12m)")
with c8:
altair_metric_chart(pod_df, "issues_closed", "Issues closed (3m/6m/12m)")
with c9:
altair_metric_chart(pod_df, "dev_msgs", "Dev messages (3m/6m/12m)")
# ============================
# UI (same layout)
# ============================
st.set_page_config(layout="wide")
st.title("Incubator Health — combined view (3m/6m/12m)")
if "selected_podling_key" not in st.session_state:
st.session_state["selected_podling_key"] = None
with st.sidebar:
reports_dir = st.text_input("Reports directory", "reports")
max_commits = st.slider("Max report runs (commits touching reports/)", 10, 300, 120, 10)
rebuild = st.button("Rebuild dataset (clear cache)")
try:
repo_root = resolve_repo_root()
except RuntimeError as e:
st.error(str(e))
st.stop()
if rebuild:
build_dataset.clear()
df = build_dataset(str(repo_root), reports_dir, max_commits)
if df.empty:
st.warning("No data parsed. Check reports_dir.")
try:
st.write("cwd:", str(Path.cwd().resolve()))
st.write("repo_root:", str(repo_root))
st.write("reports_dir:", reports_dir)
st.write("reports_dir exists on disk:", (Path(repo_root) / reports_dir).exists())
out = run_git(repo_root, ["ls-tree", "-r", "--name-only", "HEAD", reports_dir])
st.write("git ls-tree (first 20 lines):", out.splitlines()[:20])
except Exception as e:
st.error(f"Debug: {e}")
st.stop()
# STRICT latest-only combo for indicators/availability
combo = latest_combo(df)
combo["signals"] = combo.apply(build_signals_latest_only, axis=1)
combo["primary_count"] = combo["signals"].apply(lambda xs: sum(1 for x in xs if x["level"] == PRIMARY))
combo["secondary_count"] = combo["signals"].apply(lambda xs: sum(1 for x in xs if x["level"] == SECONDARY))
combo["fyi_count"] = combo["signals"].apply(lambda xs: sum(1 for x in xs if x["level"] == FYI))
combo["signal_summary"] = combo["signals"].apply(signals_summary)
tabs = st.tabs(["📋 Podling queue", "📈 Charts", "📝 Commentary builder"])
with tabs[0]:
st.subheader("Podling queue (latest report per podling)")
c1, c2, c3 = st.columns(3)
with c1:
show_level = st.selectbox(
"Show",
["Primary only", "Primary + Secondary", "All (incl FYI)"],
index=1,
)
with c2:
contains = st.text_input("Filter signals containing (optional)", value="")
with c3:
sort_by = st.selectbox("Sort by", ["Most primary", "Most secondary", "Podling name"], index=0)
q = combo.copy()
if show_level == "Primary only":
q = q[q["primary_count"] >= 1]
elif show_level == "Primary + Secondary":
q = q[(q["primary_count"] + q["secondary_count"]) >= 1]
else:
# All projects (no filtering-out by default would also be fine, but keep your UI)
q = q.copy()
if contains.strip():
needle = contains.strip().lower()
q = q[q["signal_summary"].str.lower().str.contains(needle, na=False)]
if sort_by == "Most primary":
q = q.sort_values(["primary_count", "secondary_count", "podling_display"], ascending=[False, False, True])
elif sort_by == "Most secondary":
q = q.sort_values(["secondary_count", "primary_count", "podling_display"], ascending=[False, False, True])
else:
q = q.sort_values(["podling_display"], ascending=[True])
cols = [
"podling_display",
"primary_count",
"secondary_count",
"fyi_count",
"signal_summary",
"releases_3m", "releases_6m", "releases_12m",
"commits_3m", "commits_6m", "commits_12m",
"prs_merged_3m", "prs_merged_6m", "prs_merged_12m",
"dev_unique_posters_3m", "dev_unique_posters_6m", "dev_unique_posters_12m",
"dev_msgs_3m", "dev_msgs_6m", "dev_msgs_12m",
"median_merge_days_3m", "median_merge_days_6m", "median_merge_days_12m",
"bus50_3m", "bus50_6m", "bus50_12m",
]
present = [c for c in cols if c in q.columns]
try:
event = st.dataframe(
q[present],
use_container_width=True,
height=520,
selection_mode="single-row",
on_select="rerun",
)
sel_rows = event.selection.rows
if sel_rows:
# map selected display back to key deterministically via the selected row in q
st.session_state["selected_podling_key"] = q.iloc[sel_rows[0]]["podling_key"]
except Exception:
st.info(
"Row selection isn’t available in this Streamlit version. "
"Use the dropdown below."
)
# fallback list: ALL projects
pods_all = (
combo[["podling_key", "podling_display"]]
.drop_duplicates()
.sort_values("podling_display")
.reset_index(drop=True)
)
display = pods_all["podling_display"].tolist()
# no extra widgets elsewhere; only used when dataframe selection is unavailable
chosen_display = st.selectbox("Select podling", display, key="fallback_pod_select")
chosen_key = pods_all[pods_all["podling_display"] == chosen_display].iloc[0]["podling_key"]
st.session_state["selected_podling_key"] = chosen_key
with tabs[1]:
st.subheader("Charts")
pods_all = (
combo[["podling_key", "podling_display"]]
.drop_duplicates()
.sort_values("podling_display")
.reset_index(drop=True)
)
options = pods_all["podling_key"].tolist()
fmt = dict(zip(pods_all["podling_key"], pods_all["podling_display"]))
sel_key = st.session_state.get("selected_podling_key")
if sel_key is None and options:
sel_key = options[0]
st.session_state["selected_podling_key"] = sel_key
# keep the charts selectbox in sync WITHOUT warnings: set session_state value, do not pass index
if "charts_pod_key" not in st.session_state:
st.session_state["charts_pod_key"] = sel_key
elif sel_key is not None and st.session_state["charts_pod_key"] != sel_key and sel_key in options:
st.session_state["charts_pod_key"] = sel_key
chosen_key = st.selectbox(
"Podling",
options,
format_func=lambda k: fmt.get(k, k),
key="charts_pod_key",
)
st.session_state["selected_podling_key"] = chosen_key
pod_df = df[df["podling_key"] == chosen_key].sort_values("snapshot_date")
charts_panel(pod_df)
with tabs[2]:
st.subheader("Commentary builder (copyable)")
pods_all = (
combo[["podling_key", "podling_display"]]
.drop_duplicates()
.sort_values("podling_display")
.reset_index(drop=True)
)
options = pods_all["podling_key"].tolist()
fmt = dict(zip(pods_all["podling_key"], pods_all["podling_display"]))
sel_key = st.session_state.get("selected_podling_key")
if sel_key is None and options:
sel_key = options[0]
st.session_state["selected_podling_key"] = sel_key
if "commentary_pod_key" not in st.session_state:
st.session_state["commentary_pod_key"] = sel_key
elif sel_key is not None and st.session_state["commentary_pod_key"] != sel_key and sel_key in options:
st.session_state["commentary_pod_key"] = sel_key
chosen_key = st.selectbox(
"Podling",
options,
format_func=lambda k: fmt.get(k, k),
key="commentary_pod_key",
)
st.session_state["selected_podling_key"] = chosen_key
row = combo[combo["podling_key"] == chosen_key].iloc[0]
pod_display = fmt.get(chosen_key, chosen_key)
st.code(draft_commentary_from_latest(pod_display, row), language="markdown")