Cuts release 0.20.0. ## Highlights - X-Request-ID header on every response + sanitized to [A-Za-z0-9_-] (CRLF log-forging mitigation) - Error pages (HTML + JSON 500) surface request_id for support tickets - Dev debug toolbar gated by DEBUG=1 — fastapi-debug-toolbar with custom DuckDBPanel - Centralized app.logging_config.setup_logging() replaces 23 scattered basicConfig calls - Telegram bot drops bot.log file — stdout only (BREAKING) ## Devin findings addressed - BUG_0001: .env.template no longer claims FastAPI debug=True - BUG_0002: subprocess extractor logs INFO to stderr again - ANALYSIS_0003: _wants_html no longer matches Accept: */* (curl gets JSON as before) - BUG on b1c6ee9: HTML 500 page no longer leaks str(exc) in production - BUG on b13d2fe: 2 CLAUDE.md compliance flags (transform.py + ws_gateway) accepted as scope-limited logging refactor — follow-up to update CLAUDE.md if needed See CHANGELOG [0.20.0] for full notes.
249 lines
6.7 KiB
Python
249 lines
6.7 KiB
Python
"""
|
|
Generate a sample report chart for /test command.
|
|
|
|
Creates a polished demo dashboard image with fake data
|
|
to showcase what notifications can look like.
|
|
"""
|
|
|
|
import os
|
|
import random
|
|
import tempfile
|
|
from datetime import datetime, timedelta
|
|
|
|
import numpy as np
|
|
|
|
|
|
# Brand colors
|
|
COLOR_PRIMARY = "#0073D1"
|
|
COLOR_DARK = "#1A253C"
|
|
COLOR_SUCCESS = "#10B77F"
|
|
COLOR_WARNING = "#F59F0A"
|
|
COLOR_ERROR = "#EA580C"
|
|
COLOR_BG = "#F5F7FA"
|
|
COLOR_SURFACE = "#FFFFFF"
|
|
COLOR_GRAY = "#6B7280"
|
|
|
|
|
|
def generate_test_report(username: str) -> tuple[str, str]:
|
|
"""Generate a test report image and return (image_path, caption).
|
|
|
|
Creates a professional-looking dashboard with:
|
|
- Revenue trend line (7 days)
|
|
- KPI summary bar
|
|
- Top metrics breakdown
|
|
"""
|
|
import matplotlib
|
|
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
import matplotlib.dates as mdates
|
|
from matplotlib.patches import FancyBboxPatch
|
|
|
|
rng = random.Random(42)
|
|
|
|
# Generate fake data - 14 days of revenue
|
|
today = datetime.now()
|
|
dates = [today - timedelta(days=i) for i in range(13, -1, -1)]
|
|
base_revenue = 52000
|
|
revenues = [base_revenue + rng.gauss(0, 5000) + i * 300 for i, _ in enumerate(dates)]
|
|
# Make today slightly lower for "alert" feel
|
|
revenues[-1] = revenues[-2] * 0.82
|
|
|
|
# KPI data
|
|
today_rev = revenues[-1]
|
|
yesterday_rev = revenues[-2]
|
|
week_avg = np.mean(revenues[-7:])
|
|
total_7d = sum(revenues[-7:])
|
|
change_pct = ((today_rev - yesterday_rev) / yesterday_rev) * 100
|
|
|
|
# Create figure with subplots
|
|
fig = plt.figure(figsize=(10, 7), facecolor=COLOR_BG)
|
|
fig.subplots_adjust(top=0.88, bottom=0.08, left=0.08, right=0.95, hspace=0.45)
|
|
|
|
# Title
|
|
fig.text(
|
|
0.08,
|
|
0.95,
|
|
"Data Analyst Report",
|
|
fontsize=18,
|
|
fontweight="bold",
|
|
color=COLOR_DARK,
|
|
fontfamily="sans-serif",
|
|
)
|
|
fig.text(
|
|
0.08,
|
|
0.91,
|
|
f"{today.strftime('%B %d, %Y')} | Demo report for {username}",
|
|
fontsize=11,
|
|
color=COLOR_GRAY,
|
|
fontfamily="sans-serif",
|
|
)
|
|
|
|
# --- KPI Cards (top row) ---
|
|
kpi_data = [
|
|
("Today's Revenue", f"${today_rev:,.0f}", f"{change_pct:+.1f}%", change_pct >= 0),
|
|
("7-Day Average", f"${week_avg:,.0f}", "", True),
|
|
("7-Day Total", f"${total_7d:,.0f}", "", True),
|
|
("Active Projects", f"{rng.randint(180, 220)}", "+12", True),
|
|
]
|
|
|
|
for i, (label, value, badge, is_positive) in enumerate(kpi_data):
|
|
ax_kpi = fig.add_axes([0.08 + i * 0.225, 0.72, 0.19, 0.14])
|
|
ax_kpi.set_xlim(0, 1)
|
|
ax_kpi.set_ylim(0, 1)
|
|
ax_kpi.axis("off")
|
|
|
|
# Card background
|
|
card = FancyBboxPatch(
|
|
(0, 0),
|
|
1,
|
|
1,
|
|
boxstyle="round,pad=0.05",
|
|
facecolor=COLOR_SURFACE,
|
|
edgecolor="#E5E7EB",
|
|
linewidth=1,
|
|
)
|
|
ax_kpi.add_patch(card)
|
|
|
|
ax_kpi.text(0.1, 0.7, label, fontsize=8, color=COLOR_GRAY, fontfamily="sans-serif")
|
|
ax_kpi.text(0.1, 0.25, value, fontsize=16, fontweight="bold", color=COLOR_DARK, fontfamily="sans-serif")
|
|
|
|
if badge:
|
|
badge_color = COLOR_SUCCESS if is_positive else COLOR_ERROR
|
|
ax_kpi.text(
|
|
0.9, 0.25, badge, fontsize=9, fontweight="bold", color=badge_color, ha="right", fontfamily="sans-serif"
|
|
)
|
|
|
|
# --- Revenue Chart ---
|
|
ax_chart = fig.add_subplot(2, 1, 2)
|
|
ax_chart.set_facecolor(COLOR_SURFACE)
|
|
|
|
# Plot area fill
|
|
ax_chart.fill_between(
|
|
dates,
|
|
revenues,
|
|
alpha=0.1,
|
|
color=COLOR_PRIMARY,
|
|
)
|
|
|
|
# Main line
|
|
ax_chart.plot(
|
|
dates[:-1],
|
|
revenues[:-1],
|
|
color=COLOR_PRIMARY,
|
|
linewidth=2.5,
|
|
solid_capstyle="round",
|
|
)
|
|
|
|
# Today's point (highlighted)
|
|
ax_chart.plot(
|
|
dates[-1],
|
|
revenues[-1],
|
|
"o",
|
|
color=COLOR_ERROR,
|
|
markersize=8,
|
|
zorder=5,
|
|
)
|
|
ax_chart.plot(
|
|
dates[-1],
|
|
revenues[-1],
|
|
"o",
|
|
color=COLOR_ERROR,
|
|
markersize=14,
|
|
alpha=0.2,
|
|
zorder=4,
|
|
)
|
|
|
|
# Dashed line connecting to today
|
|
ax_chart.plot(
|
|
dates[-2:],
|
|
revenues[-2:],
|
|
color=COLOR_ERROR,
|
|
linewidth=2,
|
|
linestyle="--",
|
|
alpha=0.7,
|
|
)
|
|
|
|
# Average line
|
|
ax_chart.axhline(
|
|
y=week_avg,
|
|
color=COLOR_WARNING,
|
|
linewidth=1,
|
|
linestyle=":",
|
|
alpha=0.8,
|
|
label=f"7d avg: ${week_avg:,.0f}",
|
|
)
|
|
|
|
# Styling
|
|
ax_chart.set_title(
|
|
"Daily Revenue (14 days)",
|
|
fontsize=13,
|
|
fontweight="bold",
|
|
color=COLOR_DARK,
|
|
loc="left",
|
|
pad=12,
|
|
fontfamily="sans-serif",
|
|
)
|
|
ax_chart.xaxis.set_major_formatter(mdates.DateFormatter("%b %d"))
|
|
ax_chart.xaxis.set_major_locator(mdates.DayLocator(interval=2))
|
|
plt.setp(ax_chart.xaxis.get_majorticklabels(), rotation=0, fontsize=9, color=COLOR_GRAY)
|
|
plt.setp(ax_chart.yaxis.get_majorticklabels(), fontsize=9, color=COLOR_GRAY)
|
|
|
|
ax_chart.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f"${x / 1000:.0f}k"))
|
|
|
|
ax_chart.spines["top"].set_visible(False)
|
|
ax_chart.spines["right"].set_visible(False)
|
|
ax_chart.spines["left"].set_color("#E5E7EB")
|
|
ax_chart.spines["bottom"].set_color("#E5E7EB")
|
|
ax_chart.tick_params(colors="#E5E7EB")
|
|
ax_chart.grid(axis="y", color="#F3F4F6", linewidth=0.8)
|
|
|
|
ax_chart.legend(
|
|
loc="upper left",
|
|
fontsize=9,
|
|
frameon=False,
|
|
labelcolor=COLOR_GRAY,
|
|
)
|
|
|
|
# Annotate today's drop
|
|
ax_chart.annotate(
|
|
f"${today_rev:,.0f}\n({change_pct:+.1f}%)",
|
|
xy=(dates[-1], revenues[-1]),
|
|
xytext=(30, 25),
|
|
textcoords="offset points",
|
|
fontsize=9,
|
|
fontweight="bold",
|
|
color=COLOR_ERROR,
|
|
fontfamily="sans-serif",
|
|
arrowprops=dict(arrowstyle="->", color=COLOR_ERROR, lw=1.2),
|
|
bbox=dict(boxstyle="round,pad=0.4", facecolor="white", edgecolor=COLOR_ERROR, alpha=0.9),
|
|
)
|
|
|
|
# Footer
|
|
fig.text(
|
|
0.5,
|
|
0.01,
|
|
"This is a demo report. Set up real notifications with your AI assistant.",
|
|
fontsize=8,
|
|
color=COLOR_GRAY,
|
|
ha="center",
|
|
fontstyle="italic",
|
|
fontfamily="sans-serif",
|
|
)
|
|
|
|
# Save
|
|
chart_path = os.path.join(
|
|
tempfile.gettempdir(),
|
|
f"notify_test_{username}_{datetime.now():%Y%m%d%H%M%S}.png",
|
|
)
|
|
fig.savefig(chart_path, dpi=180, bbox_inches="tight", facecolor=COLOR_BG)
|
|
plt.close(fig)
|
|
|
|
caption = (
|
|
f"*Test Report for {username}*\n"
|
|
f"Revenue today: ${today_rev:,.0f} ({change_pct:+.1f}%)\n"
|
|
f"7d avg: ${week_avg:,.0f}\n\n"
|
|
f"This is a demo. Ask your AI assistant to create real notifications!"
|
|
)
|
|
|
|
return chart_path, caption
|