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(
"""
""",
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"""
{project["name"]}
{project["description"]}
Public portfolio dashboard. Aggregated statistics only.
""",
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'{item}' for item in project["stack"]),
unsafe_allow_html=True,
)