chore: add standup dashboard with CI audit integration
Dash app at .codeflash/standups/ that pulls GitHub PR data across codeflash, codeflash-internal, codeflash-agent, and github-workflows, renders standup notes from markdown, and includes CI audit report.
This commit is contained in:
parent
3ee9c22c8e
commit
fdfddea297
5 changed files with 704 additions and 0 deletions
475
.codeflash/standups/app.py
Normal file
475
.codeflash/standups/app.py
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
"""Codeflash Standup Dashboard
|
||||
|
||||
Two-tab report served at http://localhost:8052/:
|
||||
1. Today — hero metrics, open PRs, recently merged PRs, standup notes
|
||||
2. History — past standup entries with PR activity timeline
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import plotly.graph_objects as go
|
||||
from dash import Dash, Input, Output, dash_table, dcc, html
|
||||
from theme import (
|
||||
ACCENT,
|
||||
AMBER,
|
||||
BG,
|
||||
BLUE,
|
||||
CARD,
|
||||
CARD_BORDER,
|
||||
FONT,
|
||||
GRAY,
|
||||
GREEN,
|
||||
GRID_OVERLAY,
|
||||
MONO,
|
||||
PURPLE,
|
||||
SLATE,
|
||||
TABLE_CELL,
|
||||
TABLE_DATA,
|
||||
TABLE_DATA_CONDITIONAL,
|
||||
TABLE_HEADER,
|
||||
TABLE_WRAP,
|
||||
WHITE,
|
||||
)
|
||||
|
||||
DATA_FILE = Path(__file__).parent / "data.json"
|
||||
|
||||
|
||||
def load_data() -> dict:
|
||||
return json.loads(DATA_FILE.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
app = Dash(__name__)
|
||||
app.title = "Codeflash Standup"
|
||||
|
||||
|
||||
def hero_card(label: str, value: str | int, color: str = ACCENT) -> html.Div:
|
||||
return html.Div(
|
||||
[
|
||||
html.Div(str(value), style={"fontSize": "36px", "fontWeight": "700", "color": color, "fontFamily": MONO}),
|
||||
html.Div(label, style={"fontSize": "13px", "color": GRAY, "marginTop": "4px"}),
|
||||
],
|
||||
style={**CARD, "textAlign": "center", "minWidth": "160px", "flex": "1"},
|
||||
)
|
||||
|
||||
|
||||
def pr_table(prs: list[dict], table_id: str) -> html.Div:
|
||||
rows = []
|
||||
for pr in prs:
|
||||
rows.append(
|
||||
{
|
||||
"Repo": pr["repo"],
|
||||
"#": pr["number"],
|
||||
"Title": pr["title"],
|
||||
"Author": pr["author"],
|
||||
"Updated": pr["updated_at"][:10],
|
||||
}
|
||||
)
|
||||
if not rows:
|
||||
return html.Div("No PRs", style={"color": GRAY, "padding": "16px"})
|
||||
return html.Div(
|
||||
dash_table.DataTable(
|
||||
id=table_id,
|
||||
columns=[{"name": c, "id": c} for c in ["Repo", "#", "Title", "Author", "Updated"]],
|
||||
data=rows,
|
||||
style_header=TABLE_HEADER,
|
||||
style_cell=TABLE_CELL,
|
||||
style_data=TABLE_DATA,
|
||||
style_data_conditional=TABLE_DATA_CONDITIONAL,
|
||||
page_size=15,
|
||||
style_table={"overflowX": "auto"},
|
||||
),
|
||||
style=TABLE_WRAP,
|
||||
)
|
||||
|
||||
|
||||
def notes_section(notes: list[dict]) -> html.Div:
|
||||
if not notes:
|
||||
return html.Div("No standup notes yet.", style={"color": GRAY, "padding": "16px"})
|
||||
children = []
|
||||
for note in notes:
|
||||
title = note.get("title", note["date"])
|
||||
sections = note.get("sections", {})
|
||||
items = []
|
||||
for section_name, bullets in sections.items():
|
||||
items.append(
|
||||
html.H4(section_name.title(), style={"color": ACCENT, "margin": "16px 0 8px 0", "fontSize": "16px"})
|
||||
)
|
||||
for i, b in enumerate(bullets, 1):
|
||||
items.append(
|
||||
html.Li(
|
||||
f"{i}. {b}",
|
||||
style={"color": SLATE, "fontSize": "16px", "lineHeight": "1.6", "marginBottom": "8px"},
|
||||
)
|
||||
)
|
||||
children.append(
|
||||
html.Div(
|
||||
[
|
||||
html.H3(title, style={"color": WHITE, "fontSize": "16px", "marginBottom": "8px"}),
|
||||
html.Ol(items, style={"listStyle": "none", "paddingLeft": "20px"}),
|
||||
],
|
||||
style={**CARD, "marginBottom": "16px"},
|
||||
)
|
||||
)
|
||||
return html.Div(children)
|
||||
|
||||
|
||||
def repo_breakdown_chart(prs: list[dict], title: str) -> dcc.Graph:
|
||||
repo_counts: dict[str, int] = {}
|
||||
for pr in prs:
|
||||
repo_counts[pr["repo"]] = repo_counts.get(pr["repo"], 0) + 1
|
||||
repos = list(repo_counts.keys())
|
||||
counts = list(repo_counts.values())
|
||||
colors = [ACCENT, BLUE, GREEN, PURPLE, AMBER]
|
||||
fig = go.Figure(
|
||||
go.Bar(
|
||||
x=repos,
|
||||
y=counts,
|
||||
marker_color=colors[: len(repos)],
|
||||
text=counts,
|
||||
textposition="outside",
|
||||
textfont={"color": WHITE, "size": 14},
|
||||
)
|
||||
)
|
||||
fig.update_layout(
|
||||
title={"text": title, "font": {"color": WHITE, "size": 16}},
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font={"color": SLATE, "family": FONT},
|
||||
xaxis={"gridcolor": CARD_BORDER, "color": GRAY},
|
||||
yaxis={"gridcolor": CARD_BORDER, "color": GRAY, "dtick": 1},
|
||||
margin={"l": 40, "r": 20, "t": 50, "b": 40},
|
||||
height=300,
|
||||
)
|
||||
return dcc.Graph(figure=fig, config={"displayModeBar": False})
|
||||
|
||||
|
||||
def before_after_chart(labels: list[str], before: list[float], after: list[float], title: str) -> dcc.Graph:
|
||||
fig = go.Figure()
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
name="Before",
|
||||
x=labels,
|
||||
y=before,
|
||||
marker_color="#f87171",
|
||||
text=before,
|
||||
textposition="outside",
|
||||
textfont={"color": WHITE, "size": 12},
|
||||
)
|
||||
)
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
name="After",
|
||||
x=labels,
|
||||
y=after,
|
||||
marker_color=GREEN,
|
||||
text=after,
|
||||
textposition="outside",
|
||||
textfont={"color": WHITE, "size": 12},
|
||||
)
|
||||
)
|
||||
fig.update_layout(
|
||||
barmode="group",
|
||||
title={"text": title, "font": {"color": WHITE, "size": 16}},
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font={"color": SLATE, "family": FONT},
|
||||
xaxis={"gridcolor": CARD_BORDER, "color": GRAY},
|
||||
yaxis={"gridcolor": CARD_BORDER, "color": GRAY},
|
||||
legend={"font": {"color": SLATE}},
|
||||
margin={"l": 40, "r": 20, "t": 50, "b": 40},
|
||||
height=320,
|
||||
)
|
||||
return dcc.Graph(figure=fig, config={"displayModeBar": False})
|
||||
|
||||
|
||||
def build_ci_audit_tab(audit: dict) -> html.Div:
|
||||
billing = audit.get("billing", {})
|
||||
ops = audit.get("operational_before_after", {})
|
||||
run_vol = audit.get("run_volume", {})
|
||||
|
||||
hero_row = html.Div(
|
||||
[
|
||||
hero_card(
|
||||
"Workflows", f"{ops.get('workflow_files', [0, 0])[0]} → {ops.get('workflow_files', [0, 0])[1]}", GREEN
|
||||
),
|
||||
hero_card(
|
||||
"Required Checks",
|
||||
f"{ops.get('required_checks', [0, 0])[0]} → {ops.get('required_checks', [0, 0])[1]}",
|
||||
GREEN,
|
||||
),
|
||||
hero_card("Runs Eliminated/mo", f"{run_vol.get('total_eliminated_monthly', 0):,}", ACCENT),
|
||||
hero_card("Annual Savings", f"${billing.get('overage_saved_annual_usd', 0):,}", GREEN),
|
||||
],
|
||||
style={"display": "flex", "gap": "16px", "marginBottom": "32px", "flexWrap": "wrap"},
|
||||
)
|
||||
|
||||
charts_row = html.Div(
|
||||
[
|
||||
html.Div(
|
||||
before_after_chart(
|
||||
["Workflow Files", "Required Checks"],
|
||||
[ops.get("workflow_files", [0])[0], ops.get("required_checks", [0])[0]],
|
||||
[ops.get("workflow_files", [0, 0])[1], ops.get("required_checks", [0, 0])[1]],
|
||||
"Operational Simplification",
|
||||
),
|
||||
style={"flex": "1", **CARD},
|
||||
),
|
||||
html.Div(
|
||||
before_after_chart(
|
||||
["Monthly Run Volume"],
|
||||
[run_vol.get("codeflash_feb", 0)],
|
||||
[run_vol.get("codeflash_apr_projected", 0)],
|
||||
f"codeflash Runs ({run_vol.get('codeflash_reduction_pct', 0)}% reduction)",
|
||||
),
|
||||
style={"flex": "1", **CARD},
|
||||
),
|
||||
],
|
||||
style={"display": "flex", "gap": "16px", "marginBottom": "32px", "flexWrap": "wrap"},
|
||||
)
|
||||
|
||||
findings_rows = []
|
||||
for repo, items in audit.get("findings", {}).items():
|
||||
for f in items:
|
||||
findings_rows.append(
|
||||
{
|
||||
"Repo": repo,
|
||||
"Finding": f["finding"],
|
||||
"Impact": f["impact"],
|
||||
"Fix": f["fix"],
|
||||
"PR": f.get("pr") or "—",
|
||||
}
|
||||
)
|
||||
|
||||
findings_table = html.Div(
|
||||
[
|
||||
html.H2("Findings", style={"color": WHITE, "fontSize": "20px", "marginBottom": "16px"}),
|
||||
html.Div(
|
||||
dash_table.DataTable(
|
||||
id="findings-table",
|
||||
columns=[{"name": c, "id": c} for c in ["Repo", "Finding", "Impact", "Fix", "PR"]],
|
||||
data=findings_rows,
|
||||
style_header=TABLE_HEADER,
|
||||
style_cell={**TABLE_CELL, "whiteSpace": "normal", "maxWidth": "300px"},
|
||||
style_data=TABLE_DATA,
|
||||
style_data_conditional=TABLE_DATA_CONDITIONAL,
|
||||
page_size=20,
|
||||
style_table={"overflowX": "auto"},
|
||||
),
|
||||
style=TABLE_WRAP,
|
||||
),
|
||||
],
|
||||
style={"marginBottom": "32px"},
|
||||
)
|
||||
|
||||
audit_prs = audit.get("prs_merged", [])
|
||||
pr_rows = [{"PR": p["pr"], "Repo": p["repo"], "Date": p["date"], "Title": p["title"]} for p in audit_prs]
|
||||
prs_table = html.Div(
|
||||
[
|
||||
html.H2("Audit PRs Merged", style={"color": WHITE, "fontSize": "20px", "marginBottom": "16px"}),
|
||||
html.Div(
|
||||
dash_table.DataTable(
|
||||
id="audit-prs-table",
|
||||
columns=[{"name": c, "id": c} for c in ["PR", "Repo", "Date", "Title"]],
|
||||
data=pr_rows,
|
||||
style_header=TABLE_HEADER,
|
||||
style_cell=TABLE_CELL,
|
||||
style_data=TABLE_DATA,
|
||||
style_data_conditional=TABLE_DATA_CONDITIONAL,
|
||||
page_size=15,
|
||||
style_table={"overflowX": "auto"},
|
||||
),
|
||||
style=TABLE_WRAP,
|
||||
),
|
||||
],
|
||||
style={"marginBottom": "32px"},
|
||||
)
|
||||
|
||||
billing_row = html.Div(
|
||||
[
|
||||
hero_card("Overage Before (min/mo)", f"{billing.get('overage_before_min', 0):,}", "#f87171"),
|
||||
hero_card("Overage After (min/mo)", f"{billing.get('overage_after_min', 0):,}", GREEN),
|
||||
hero_card("Saved (min/mo)", f"{billing.get('overage_saved_min', 0):,}", ACCENT),
|
||||
hero_card("Saved ($/mo)", f"${billing.get('overage_saved_monthly_usd', 0):,}", GREEN),
|
||||
],
|
||||
style={"display": "flex", "gap": "16px", "marginBottom": "32px", "flexWrap": "wrap"},
|
||||
)
|
||||
|
||||
return html.Div(
|
||||
[
|
||||
html.Div(
|
||||
f"Audit: {audit.get('audit_date', '?')} — Completed: {audit.get('completion_date', '?')}",
|
||||
style={"color": GRAY, "fontSize": "13px", "marginBottom": "20px"},
|
||||
),
|
||||
hero_row,
|
||||
charts_row,
|
||||
billing_row,
|
||||
findings_table,
|
||||
prs_table,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def build_today_tab(data: dict) -> html.Div:
|
||||
summary = data["summary"]
|
||||
open_prs = data["open_prs"]
|
||||
merged_prs = data["merged_prs"]
|
||||
notes = data["notes"]
|
||||
return html.Div(
|
||||
[
|
||||
html.Div(
|
||||
[
|
||||
hero_card("Open PRs", summary["total_open"], BLUE),
|
||||
hero_card("Merged (7d)", summary["total_merged_7d"], GREEN),
|
||||
hero_card("Drafts", summary["draft_count"], AMBER),
|
||||
hero_card("Active Repos", summary["repos_with_open_prs"], PURPLE),
|
||||
],
|
||||
style={"display": "flex", "gap": "16px", "marginBottom": "32px", "flexWrap": "wrap"},
|
||||
),
|
||||
html.H2(
|
||||
[
|
||||
"Standup Notes ",
|
||||
html.Span(
|
||||
"(eventually everyone will create their own notes — a summary of what you worked on yesterday, what you'll be working on today, and any blockers)",
|
||||
style={"color": GRAY, "fontSize": "14px", "fontWeight": "400"},
|
||||
),
|
||||
],
|
||||
style={"color": WHITE, "fontSize": "20px", "marginBottom": "16px"},
|
||||
),
|
||||
notes_section(notes),
|
||||
html.Div(
|
||||
[
|
||||
html.Div(
|
||||
[
|
||||
html.H2("Open PRs", style={"color": WHITE, "fontSize": "20px", "marginBottom": "16px"}),
|
||||
pr_table(open_prs, "open-pr-table"),
|
||||
],
|
||||
style={"flex": "1", "minWidth": "0"},
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
html.H2(
|
||||
"Merged (Last 7 Days)",
|
||||
style={"color": WHITE, "fontSize": "20px", "marginBottom": "16px"},
|
||||
),
|
||||
pr_table(merged_prs, "merged-pr-table"),
|
||||
],
|
||||
style={"flex": "1", "minWidth": "0"},
|
||||
),
|
||||
],
|
||||
style={"display": "flex", "gap": "24px", "marginBottom": "32px", "flexWrap": "wrap"},
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
html.Div(repo_breakdown_chart(open_prs, "Open PRs by Repo"), style={"flex": "1", **CARD}),
|
||||
html.Div(repo_breakdown_chart(merged_prs, "Merged PRs by Repo (7d)"), style={"flex": "1", **CARD}),
|
||||
],
|
||||
style={"display": "flex", "gap": "16px", "marginBottom": "32px", "flexWrap": "wrap"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def serve_layout():
|
||||
data = load_data()
|
||||
return html.Div(
|
||||
[
|
||||
html.Div(style=GRID_OVERLAY),
|
||||
html.Div(
|
||||
[
|
||||
html.H1(
|
||||
"Codeflash Standup",
|
||||
style={"color": WHITE, "fontSize": "28px", "fontWeight": "700", "marginBottom": "4px"},
|
||||
),
|
||||
html.Div(
|
||||
f"Generated {data['generated_at'][:10]}",
|
||||
style={"color": GRAY, "fontSize": "13px", "marginBottom": "12px"},
|
||||
),
|
||||
html.Div(
|
||||
"Hi guys, I wanted to introduce a new way to do standups. Remember it's still a WIP and I will improve it based on feedback, but for now bear with it...",
|
||||
style={
|
||||
"color": WHITE,
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "500",
|
||||
"lineHeight": "1.7",
|
||||
"marginBottom": "28px",
|
||||
"padding": "20px 24px",
|
||||
"background": "rgba(255,210,39,0.08)",
|
||||
"borderLeft": f"3px solid {ACCENT}",
|
||||
"borderRadius": "8px",
|
||||
},
|
||||
),
|
||||
dcc.Tabs(
|
||||
id="tabs",
|
||||
value="today",
|
||||
children=[
|
||||
dcc.Tab(
|
||||
label="Today",
|
||||
value="today",
|
||||
style={
|
||||
"color": GRAY,
|
||||
"backgroundColor": "transparent",
|
||||
"border": "none",
|
||||
"padding": "12px 24px",
|
||||
},
|
||||
selected_style={
|
||||
"color": ACCENT,
|
||||
"backgroundColor": "transparent",
|
||||
"border": "none",
|
||||
"borderBottom": f"2px solid {ACCENT}",
|
||||
"padding": "12px 24px",
|
||||
},
|
||||
),
|
||||
dcc.Tab(
|
||||
label="CI Audit",
|
||||
value="ci_audit",
|
||||
style={
|
||||
"color": GRAY,
|
||||
"backgroundColor": "transparent",
|
||||
"border": "none",
|
||||
"padding": "12px 24px",
|
||||
},
|
||||
selected_style={
|
||||
"color": ACCENT,
|
||||
"backgroundColor": "transparent",
|
||||
"border": "none",
|
||||
"borderBottom": f"2px solid {ACCENT}",
|
||||
"padding": "12px 24px",
|
||||
},
|
||||
),
|
||||
],
|
||||
style={"marginBottom": "24px", "borderBottom": f"1px solid {CARD_BORDER}"},
|
||||
),
|
||||
dcc.Store(id="data-store", data=data),
|
||||
html.Div(id="tab-content"),
|
||||
],
|
||||
style={
|
||||
"maxWidth": "1200px",
|
||||
"margin": "0 auto",
|
||||
"padding": "40px 24px",
|
||||
"position": "relative",
|
||||
"zIndex": "1",
|
||||
},
|
||||
),
|
||||
],
|
||||
style={"backgroundColor": BG, "minHeight": "100vh", "fontFamily": FONT, "color": SLATE},
|
||||
)
|
||||
|
||||
|
||||
app.layout = serve_layout
|
||||
|
||||
|
||||
@app.callback(Output("tab-content", "children"), Input("tabs", "value"), Input("data-store", "data"))
|
||||
def render_tab(tab: str, data: dict):
|
||||
if tab == "ci_audit":
|
||||
ci_audit = data.get("ci_audit")
|
||||
return (
|
||||
build_ci_audit_tab(ci_audit)
|
||||
if ci_audit
|
||||
else html.Div("No CI audit data found.", style={"color": GRAY, "padding": "16px"})
|
||||
)
|
||||
return build_today_tab(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(port=8052, debug=True)
|
||||
124
.codeflash/standups/generate.py
Normal file
124
.codeflash/standups/generate.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
"""Pull GitHub PR data and standup notes into data.json for the Dash app."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
REPOS = ["codeflash", "codeflash-internal", "codeflash-agent", "github-workflows"]
|
||||
ORG = "codeflash-ai"
|
||||
NOTES_DIR = Path(__file__).parent / "notes"
|
||||
DATA_FILE = Path(__file__).parent / "data.json"
|
||||
CI_AUDIT_FILE = Path(__file__).parents[2] / ".." / "codeflash-agent" / "reports" / "codeflash-ci-audit" / "data.json"
|
||||
|
||||
|
||||
def gh_token() -> str:
|
||||
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN", "")
|
||||
if not token:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(["gh", "auth", "token"], capture_output=True, text=True, check=False)
|
||||
token = result.stdout.strip()
|
||||
return token
|
||||
|
||||
|
||||
def gh_headers() -> dict[str, str]:
|
||||
return {"Authorization": f"token {gh_token()}", "Accept": "application/vnd.github+json"}
|
||||
|
||||
|
||||
def fetch_prs(state: str, since: datetime | None = None) -> list[dict]:
|
||||
prs = []
|
||||
for repo in REPOS:
|
||||
url = f"https://api.github.com/repos/{ORG}/{repo}/pulls"
|
||||
params: dict[str, str] = {"state": state, "per_page": "30", "sort": "updated", "direction": "desc"}
|
||||
resp = requests.get(url, headers=gh_headers(), params=params, timeout=15)
|
||||
if resp.status_code != 200:
|
||||
continue
|
||||
for pr in resp.json():
|
||||
updated = datetime.fromisoformat(pr["updated_at"].replace("Z", "+00:00"))
|
||||
if since and updated < since:
|
||||
continue
|
||||
prs.append(
|
||||
{
|
||||
"repo": repo,
|
||||
"number": pr["number"],
|
||||
"title": pr["title"],
|
||||
"state": pr["state"],
|
||||
"author": pr["user"]["login"],
|
||||
"url": pr["html_url"],
|
||||
"created_at": pr["created_at"],
|
||||
"updated_at": pr["updated_at"],
|
||||
"merged_at": pr.get("merged_at"),
|
||||
"draft": pr.get("draft", False),
|
||||
}
|
||||
)
|
||||
return prs
|
||||
|
||||
|
||||
def parse_note(path: Path) -> dict:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
sections: dict[str, list[str]] = {}
|
||||
current = None
|
||||
title = None
|
||||
for line in text.splitlines():
|
||||
h1 = re.match(r"^#\s+(.+)", line)
|
||||
if h1 and not title:
|
||||
title = h1.group(1).strip()
|
||||
continue
|
||||
heading = re.match(r"^##\s+(.+)", line)
|
||||
if heading:
|
||||
current = heading.group(1).strip().lower()
|
||||
sections[current] = []
|
||||
elif current and line.strip().startswith("- "):
|
||||
sections[current].append(line.strip().removeprefix("- "))
|
||||
return {"date": path.stem, "title": title or path.stem, "sections": sections}
|
||||
|
||||
|
||||
def load_notes() -> list[dict]:
|
||||
if not NOTES_DIR.exists():
|
||||
return []
|
||||
notes = []
|
||||
for f in sorted(NOTES_DIR.glob("*.md"), reverse=True):
|
||||
notes.append(parse_note(f))
|
||||
return notes
|
||||
|
||||
|
||||
def main():
|
||||
now = datetime.now(timezone.utc)
|
||||
week_ago = now - timedelta(days=7)
|
||||
|
||||
open_prs = fetch_prs("open")
|
||||
closed_prs = fetch_prs("closed", since=week_ago)
|
||||
merged_prs = [pr for pr in closed_prs if pr["merged_at"]]
|
||||
notes = load_notes()
|
||||
|
||||
ci_audit = None
|
||||
resolved = CI_AUDIT_FILE.resolve()
|
||||
if resolved.exists():
|
||||
ci_audit = json.loads(resolved.read_text(encoding="utf-8"))
|
||||
|
||||
data = {
|
||||
"generated_at": now.isoformat(),
|
||||
"org": ORG,
|
||||
"repos": REPOS,
|
||||
"open_prs": open_prs,
|
||||
"merged_prs": merged_prs,
|
||||
"notes": notes,
|
||||
"summary": {
|
||||
"total_open": len(open_prs),
|
||||
"total_merged_7d": len(merged_prs),
|
||||
"draft_count": sum(1 for pr in open_prs if pr["draft"]),
|
||||
"repos_with_open_prs": len({pr["repo"] for pr in open_prs}),
|
||||
},
|
||||
"ci_audit": ci_audit,
|
||||
}
|
||||
|
||||
DATA_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
print(f"Wrote {DATA_FILE} ({len(open_prs)} open, {len(merged_prs)} merged)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
30
.codeflash/standups/notes/2026-04-23.md
Normal file
30
.codeflash/standups/notes/2026-04-23.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Kevin — 2026-04-23
|
||||
|
||||
## Done
|
||||
|
||||
- CI audit across codeflash org: disabled Actions on 200+ forks, built interactive Dash cost report showing ~$12K/yr savings
|
||||
- Scaffolded codeflash-api FastAPI rewrite (all 9 endpoints), unified sync/async code paths, set up Postgres integration tests with testcontainers, 78% coverage
|
||||
- Project governance: enabled GitHub Discussions, created parent issue #2079 for 100% test coverage, filed coverage issue on typeagent-py
|
||||
- Merged 5 PRs on codeflash: PR template + linked-issue policy (#2093), coverage CI with 58% floor (#2094), CODEOWNERS (#2095), 2 dependabot bumps
|
||||
- Triaged ~28 open PRs, posted contributor follow-ups, closed stale PRs and reopened as issues
|
||||
- Reimplemented JS function tracer from stale PR #1377 onto current main as PR #2105
|
||||
- Created PRs #244 (tests for resolve_azure_model_name) and #245 (max_retries bump) on typeagent-py
|
||||
- Bumped stale action versions across 3 repos (setup-uv, prek-action, gh-action-pypi-publish)
|
||||
- Fixed 3-month-old VSCode extension build failure from out-of-sync package-lock.json (PR #2617)
|
||||
- Extended shared ci-python-uv.yml with test-secret-env input, migrated aiservice-test job to use it (PR #2620)
|
||||
- Initialized tessl (vendored mode) across codeflash, codeflash-internal, and codeflash-agent with weekly tile update workflows
|
||||
- Created codeflash-ci-bot GitHub App for PR creation (enterprise GITHUB_TOKEN workaround)
|
||||
- Added org-level ruleset requiring PR branches to be up to date with main
|
||||
- Ran vulture dead-code audit (428/432 false positives), deleted 4 verified dead items, added vulture as dev dep
|
||||
- Cleaned up 19 stale local branches and removed associated worktrees
|
||||
|
||||
## Blocked
|
||||
|
||||
- Need more perms across the org
|
||||
|
||||
## Next
|
||||
|
||||
- Missing tessl tiles (~20 on internal, ~13 on agent) will resolve as tessl publishes them
|
||||
- Unpin claude-code-action once Bedrock OIDC regression is fixed
|
||||
- Re-enable Dependabot to address 24 known vulnerabilities
|
||||
- Improve comparator coverage (52%, identified as biggest gap)
|
||||
12
.codeflash/standups/pyproject.toml
Normal file
12
.codeflash/standups/pyproject.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[project]
|
||||
name = "codeflash-standups"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"dash[cloud]>=4.1",
|
||||
"plotly>=6.7",
|
||||
"requests>=2.31",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
standup = "generate:main"
|
||||
63
.codeflash/standups/theme.py
Normal file
63
.codeflash/standups/theme.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""Theme and styling constants for the standup report."""
|
||||
|
||||
# ── Colors (Codeflash dark - amber/zinc) ────────────────────────────────────
|
||||
ACCENT = "#ffd227"
|
||||
DARK = "#09090b"
|
||||
CARD_BG = "rgba(16,20,28,0.7)"
|
||||
CARD_BORDER = "rgba(63,63,70,0.35)"
|
||||
SLATE = "#e4e4e7"
|
||||
GRAY = "#a1a1aa"
|
||||
LIGHT_GRAY = "#71717a"
|
||||
BG = "#0d1117"
|
||||
WHITE = "#fafafa"
|
||||
GREEN = "#4ade80"
|
||||
LIGHT_GREEN = "rgba(74,222,128,0.12)"
|
||||
RED = "#f87171"
|
||||
LIGHT_RED = "rgba(248,113,113,0.12)"
|
||||
AMBER = "#fbbf24"
|
||||
BLUE = "#60a5fa"
|
||||
PURPLE = "#a78bfa"
|
||||
|
||||
GRID_BG_IMAGE = (
|
||||
"linear-gradient(to right, currentColor 1px, transparent 1px),"
|
||||
"linear-gradient(to bottom, currentColor 1px, transparent 1px)"
|
||||
)
|
||||
GRID_BG_SIZE = "48px 48px"
|
||||
GRID_OVERLAY = {
|
||||
"position": "fixed",
|
||||
"top": 0,
|
||||
"left": 0,
|
||||
"right": 0,
|
||||
"bottom": 0,
|
||||
"backgroundImage": GRID_BG_IMAGE,
|
||||
"backgroundSize": GRID_BG_SIZE,
|
||||
"opacity": "0.05",
|
||||
"pointerEvents": "none",
|
||||
"zIndex": "0",
|
||||
}
|
||||
|
||||
CARD = {"background": CARD_BG, "borderRadius": "16px", "padding": "28px 32px", "border": f"1px solid {CARD_BORDER}"}
|
||||
FONT = "'Inter', system-ui, -apple-system, sans-serif"
|
||||
MONO = "'JetBrains Mono', 'Menlo', monospace"
|
||||
|
||||
TABLE_HEADER: dict[str, str] = {
|
||||
"backgroundColor": "rgba(24,24,27,0.8)",
|
||||
"color": ACCENT,
|
||||
"fontWeight": "600",
|
||||
"fontSize": "13px",
|
||||
"padding": "12px 16px",
|
||||
"borderBottom": f"1px solid {CARD_BORDER}",
|
||||
}
|
||||
TABLE_CELL: dict[str, str] = {
|
||||
"textAlign": "left",
|
||||
"padding": "12px 16px",
|
||||
"fontSize": "13px",
|
||||
"fontFamily": FONT,
|
||||
"border": "none",
|
||||
"color": SLATE,
|
||||
}
|
||||
TABLE_DATA: dict[str, str] = {"backgroundColor": "rgba(24,24,27,0.5)", "color": SLATE}
|
||||
TABLE_DATA_CONDITIONAL: list[dict[str, object]] = [
|
||||
{"if": {"row_index": "odd"}, "backgroundColor": "rgba(31,31,35,0.6)"}
|
||||
]
|
||||
TABLE_WRAP: dict[str, str] = {"borderRadius": "16px", "overflow": "hidden", "border": f"1px solid {CARD_BORDER}"}
|
||||
Loading…
Reference in a new issue