Spaces:
Running
Running
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| import pandas as pd | |
| import plotly.express as px | |
| import streamlit as st | |
| DATA_PATH = Path(__file__).with_name("public_stats.json") | |
| st.set_page_config( | |
| page_title="MapToSelf Public Stats", | |
| page_icon="MapToSelf", | |
| layout="wide", | |
| ) | |
| st.markdown( | |
| """ | |
| <style> | |
| .block-container { padding-top: 2rem; max-width: 1180px; } | |
| [data-testid="stMetricValue"] { font-size: 1.75rem; } | |
| .hero { | |
| padding: 1.4rem 0 0.8rem 0; | |
| border-bottom: 1px solid rgba(128,128,128,.2); | |
| margin-bottom: 1.2rem; | |
| } | |
| .muted { color: #6b7280; font-size: .92rem; } | |
| .pill { | |
| display: inline-block; | |
| padding: .24rem .58rem; | |
| margin: .12rem .18rem .12rem 0; | |
| border: 1px solid rgba(128,128,128,.25); | |
| border-radius: 999px; | |
| font-size: .86rem; | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def load_data() -> dict: | |
| if not DATA_PATH.exists(): | |
| st.error("public_stats.json is missing. Run export_public_stats.py first.") | |
| st.stop() | |
| return json.loads(DATA_PATH.read_text(encoding="utf-8")) | |
| def df(items: list[dict]) -> pd.DataFrame: | |
| return pd.DataFrame(items or []) | |
| def metric_number(value: int | float | None) -> str: | |
| return f"{int(value or 0):,}".replace(",", " ") | |
| def line_chart(data: pd.DataFrame, x: str, y: str, title: str): | |
| if data.empty: | |
| st.info("No data yet.") | |
| return | |
| fig = px.line(data, x=x, y=y, markers=True, title=title) | |
| fig.update_layout(height=330, margin=dict(l=10, r=10, t=55, b=10)) | |
| st.plotly_chart(fig, use_container_width=True) | |
| def bar_chart(data: pd.DataFrame, x: str, y: str, title: str, color: str | None = None): | |
| if data.empty: | |
| st.info("No data yet.") | |
| return | |
| fig = px.bar(data, x=x, y=y, color=color, title=title) | |
| fig.update_layout(height=360, margin=dict(l=10, r=10, t=55, b=10)) | |
| st.plotly_chart(fig, use_container_width=True) | |
| data = load_data() | |
| project = data["project"] | |
| metrics = data["metrics"] | |
| series = data["series"] | |
| st.markdown( | |
| f""" | |
| <div class="hero"> | |
| <h1>{project["name"]}</h1> | |
| <p class="muted">{project["description"]}</p> | |
| <p class="muted">Public portfolio dashboard. Aggregated statistics only.</p> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| st.caption(f"Last updated: {data['generated_at']}") | |
| st.caption(data["public_note"]) | |
| cols = st.columns(5) | |
| cols[0].metric("Users", metric_number(metrics["users_total"])) | |
| cols[1].metric("New users, 30d", metric_number(metrics["new_30d"])) | |
| cols[2].metric("Reports generated", metric_number(metrics["reports_total"])) | |
| cols[3].metric("GPT interactions", metric_number(metrics["gpt_calls"])) | |
| cols[4].metric("Child charts", metric_number(metrics["child_charts"])) | |
| cols = st.columns(4) | |
| cols[0].metric("New users, 7d", metric_number(metrics["new_7d"])) | |
| cols[1].metric("Profiles completed", metric_number(metrics["profiles_complete"])) | |
| cols[2].metric("Natal charts", metric_number(metrics["with_chart"])) | |
| cols[3].metric("Reports, 7d", metric_number(metrics["reports_7d"])) | |
| st.divider() | |
| left, right = st.columns(2) | |
| with left: | |
| line_chart(df(series["users_by_day"]), "day", "users", "User growth over the last 30 days") | |
| with right: | |
| bar_chart(df(series["users_by_language"]), "language", "users", "Users by language") | |
| left, right = st.columns(2) | |
| with left: | |
| line_chart(df(series["reports_by_day"]), "day", "reports", "Reports generated over the last 30 days") | |
| with right: | |
| reports_by_type = df(series["reports_by_type"]) | |
| if not reports_by_type.empty: | |
| grouped_reports = ( | |
| reports_by_type.groupby("report_type", as_index=False)["reports"] | |
| .sum() | |
| .sort_values("reports", ascending=False) | |
| ) | |
| else: | |
| grouped_reports = reports_by_type | |
| bar_chart(grouped_reports, "report_type", "reports", "Reports by product area") | |
| st.subheader("Language distribution") | |
| lang_left, lang_right = st.columns(2) | |
| with lang_left: | |
| gpt_by_language = df(series["gpt_by_language"]) | |
| bar_chart(gpt_by_language, "language", "calls", "GPT interactions by language") | |
| with lang_right: | |
| reports_lang = df(series["reports_by_type"]) | |
| bar_chart(reports_lang, "language", "reports", "Reports by language", color="report_type") | |
| st.subheader("GPT-supported workflows") | |
| workflow = df(series["gpt_by_request_language"]) | |
| if workflow.empty: | |
| st.info("No workflow data yet.") | |
| else: | |
| bar_chart(workflow, "request_type", "calls", "GPT interactions by workflow and language", color="language") | |
| st.divider() | |
| st.subheader("What this project demonstrates") | |
| st.markdown( | |
| """ | |
| - A production Telegram bot with multilingual user flows in Russian, French and English. | |
| - Structured astrology calculations using Swiss Ephemeris and custom chart logic. | |
| - GPT-based long-form report generation with language-aware prompts. | |
| - SQLite-backed storage, safe aggregate analytics and private operations monitoring. | |
| - Cloud deployment on Hetzner with systemd services and a separate public dashboard. | |
| """ | |
| ) | |
| st.subheader("Stack") | |
| st.markdown( | |
| " ".join(f'<span class="pill">{item}</span>' for item in project["stack"]), | |
| unsafe_allow_html=True, | |
| ) | |