Anabeldg commited on
Commit
279bbec
·
verified ·
1 Parent(s): d24ebc4

Update public MapToSelf dashboard

Browse files
Files changed (5) hide show
  1. README.md +41 -19
  2. __pycache__/app.cpython-313.pyc +0 -0
  3. app.py +166 -0
  4. public_stats.json +480 -0
  5. requirements.txt +3 -3
README.md CHANGED
@@ -1,19 +1,41 @@
1
- ---
2
- title: MapToSelf Dashboard
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Streamlit template space
12
- ---
13
-
14
- # Welcome to Streamlit!
15
-
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
-
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MapToSelf Public Stats
2
+
3
+ Public portfolio dashboard for MapToSelf.
4
+
5
+ This dashboard is designed for recruiters, employers and collaborators. It shows
6
+ only aggregated product statistics and does not expose private operational data.
7
+
8
+ ## Privacy
9
+
10
+ The dashboard does not include:
11
+
12
+ - Telegram user IDs
13
+ - names
14
+ - birth dates or birth places
15
+ - prompts
16
+ - AI answers
17
+ - report text
18
+ - message text
19
+ - API keys
20
+ - costs
21
+ - provider or model internals
22
+
23
+ It reads only `public_stats.json`, which is generated from safe aggregate SQL
24
+ queries.
25
+
26
+ ## Run Locally
27
+
28
+ ```bash
29
+ pip install -r requirements.txt
30
+ streamlit run app.py
31
+ ```
32
+
33
+ ## Hugging Face Space
34
+
35
+ Create a Streamlit Space and upload:
36
+
37
+ - `app.py`
38
+ - `requirements.txt`
39
+ - `public_stats.json`
40
+
41
+ The Space does not need access to the production database.
__pycache__/app.cpython-313.pyc ADDED
Binary file (8.64 kB). View file
 
app.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ import plotly.express as px
8
+ import streamlit as st
9
+
10
+
11
+ DATA_PATH = Path(__file__).with_name("public_stats.json")
12
+
13
+
14
+ st.set_page_config(
15
+ page_title="MapToSelf Public Stats",
16
+ page_icon="MapToSelf",
17
+ layout="wide",
18
+ )
19
+
20
+ st.markdown(
21
+ """
22
+ <style>
23
+ .block-container { padding-top: 2rem; max-width: 1180px; }
24
+ [data-testid="stMetricValue"] { font-size: 1.75rem; }
25
+ .hero {
26
+ padding: 1.4rem 0 0.8rem 0;
27
+ border-bottom: 1px solid rgba(128,128,128,.2);
28
+ margin-bottom: 1.2rem;
29
+ }
30
+ .muted { color: #6b7280; font-size: .92rem; }
31
+ .pill {
32
+ display: inline-block;
33
+ padding: .24rem .58rem;
34
+ margin: .12rem .18rem .12rem 0;
35
+ border: 1px solid rgba(128,128,128,.25);
36
+ border-radius: 999px;
37
+ font-size: .86rem;
38
+ }
39
+ </style>
40
+ """,
41
+ unsafe_allow_html=True,
42
+ )
43
+
44
+
45
+ def load_data() -> dict:
46
+ if not DATA_PATH.exists():
47
+ st.error("public_stats.json is missing. Run export_public_stats.py first.")
48
+ st.stop()
49
+ return json.loads(DATA_PATH.read_text(encoding="utf-8"))
50
+
51
+
52
+ def df(items: list[dict]) -> pd.DataFrame:
53
+ return pd.DataFrame(items or [])
54
+
55
+
56
+ def metric_number(value: int | float | None) -> str:
57
+ return f"{int(value or 0):,}".replace(",", " ")
58
+
59
+
60
+ def line_chart(data: pd.DataFrame, x: str, y: str, title: str):
61
+ if data.empty:
62
+ st.info("No data yet.")
63
+ return
64
+ fig = px.line(data, x=x, y=y, markers=True, title=title)
65
+ fig.update_layout(height=330, margin=dict(l=10, r=10, t=55, b=10))
66
+ st.plotly_chart(fig, use_container_width=True)
67
+
68
+
69
+ def bar_chart(data: pd.DataFrame, x: str, y: str, title: str, color: str | None = None):
70
+ if data.empty:
71
+ st.info("No data yet.")
72
+ return
73
+ fig = px.bar(data, x=x, y=y, color=color, title=title)
74
+ fig.update_layout(height=360, margin=dict(l=10, r=10, t=55, b=10))
75
+ st.plotly_chart(fig, use_container_width=True)
76
+
77
+
78
+ data = load_data()
79
+ project = data["project"]
80
+ metrics = data["metrics"]
81
+ series = data["series"]
82
+
83
+ st.markdown(
84
+ f"""
85
+ <div class="hero">
86
+ <h1>{project["name"]}</h1>
87
+ <p class="muted">{project["description"]}</p>
88
+ <p class="muted">Public portfolio dashboard. Aggregated statistics only.</p>
89
+ </div>
90
+ """,
91
+ unsafe_allow_html=True,
92
+ )
93
+
94
+ st.caption(f"Last updated: {data['generated_at']}")
95
+ st.caption(data["public_note"])
96
+
97
+ cols = st.columns(5)
98
+ cols[0].metric("Users", metric_number(metrics["users_total"]))
99
+ cols[1].metric("New users, 30d", metric_number(metrics["new_30d"]))
100
+ cols[2].metric("Reports generated", metric_number(metrics["reports_total"]))
101
+ cols[3].metric("GPT interactions", metric_number(metrics["gpt_calls"]))
102
+ cols[4].metric("Child charts", metric_number(metrics["child_charts"]))
103
+
104
+ cols = st.columns(4)
105
+ cols[0].metric("New users, 7d", metric_number(metrics["new_7d"]))
106
+ cols[1].metric("Profiles completed", metric_number(metrics["profiles_complete"]))
107
+ cols[2].metric("Natal charts", metric_number(metrics["with_chart"]))
108
+ cols[3].metric("Reports, 7d", metric_number(metrics["reports_7d"]))
109
+
110
+ st.divider()
111
+
112
+ left, right = st.columns(2)
113
+ with left:
114
+ line_chart(df(series["users_by_day"]), "day", "users", "User growth over the last 30 days")
115
+ with right:
116
+ bar_chart(df(series["users_by_language"]), "language", "users", "Users by language")
117
+
118
+ left, right = st.columns(2)
119
+ with left:
120
+ line_chart(df(series["reports_by_day"]), "day", "reports", "Reports generated over the last 30 days")
121
+ with right:
122
+ reports_by_type = df(series["reports_by_type"])
123
+ if not reports_by_type.empty:
124
+ grouped_reports = (
125
+ reports_by_type.groupby("report_type", as_index=False)["reports"]
126
+ .sum()
127
+ .sort_values("reports", ascending=False)
128
+ )
129
+ else:
130
+ grouped_reports = reports_by_type
131
+ bar_chart(grouped_reports, "report_type", "reports", "Reports by product area")
132
+
133
+ st.subheader("Language distribution")
134
+ lang_left, lang_right = st.columns(2)
135
+ with lang_left:
136
+ gpt_by_language = df(series["gpt_by_language"])
137
+ bar_chart(gpt_by_language, "language", "calls", "GPT interactions by language")
138
+ with lang_right:
139
+ reports_lang = df(series["reports_by_type"])
140
+ bar_chart(reports_lang, "language", "reports", "Reports by language", color="report_type")
141
+
142
+ st.subheader("GPT-supported workflows")
143
+ workflow = df(series["gpt_by_request_language"])
144
+ if workflow.empty:
145
+ st.info("No workflow data yet.")
146
+ else:
147
+ bar_chart(workflow, "request_type", "calls", "GPT interactions by workflow and language", color="language")
148
+
149
+ st.divider()
150
+
151
+ st.subheader("What this project demonstrates")
152
+ st.markdown(
153
+ """
154
+ - A production Telegram bot with multilingual user flows in Russian, French and English.
155
+ - Structured astrology calculations using Swiss Ephemeris and custom chart logic.
156
+ - GPT-based long-form report generation with language-aware prompts.
157
+ - SQLite-backed storage, safe aggregate analytics and private operations monitoring.
158
+ - Cloud deployment on Hetzner with systemd services and a separate public dashboard.
159
+ """
160
+ )
161
+
162
+ st.subheader("Stack")
163
+ st.markdown(
164
+ " ".join(f'<span class="pill">{item}</span>' for item in project["stack"]),
165
+ unsafe_allow_html=True,
166
+ )
public_stats.json ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "generated_at": "2026-06-13T12:54:40Z",
3
+ "public_note": "Aggregated portfolio statistics only. No personal data, prompts, answers, costs, IDs, birth data, or message text.",
4
+ "project": {
5
+ "name": "MapToSelf",
6
+ "description": "A multilingual Telegram bot that generates astrology-based self-reflection reports from natal chart data.",
7
+ "stack": [
8
+ "Python",
9
+ "python-telegram-bot",
10
+ "GPT API",
11
+ "SQLite",
12
+ "Swiss Ephemeris",
13
+ "Streamlit",
14
+ "Hetzner Cloud"
15
+ ],
16
+ "languages": [
17
+ "Russian",
18
+ "French",
19
+ "English"
20
+ ]
21
+ },
22
+ "metrics": {
23
+ "users_total": 29,
24
+ "new_7d": 25,
25
+ "new_30d": 29,
26
+ "profiles_complete": 29,
27
+ "with_chart": 29,
28
+ "reports_total": 83,
29
+ "reports_7d": 76,
30
+ "gpt_calls": 114,
31
+ "gpt_calls_7d": 111,
32
+ "child_charts": 4
33
+ },
34
+ "series": {
35
+ "users_by_day": [
36
+ {
37
+ "day": "2026-05-15",
38
+ "users": 0
39
+ },
40
+ {
41
+ "day": "2026-05-16",
42
+ "users": 0
43
+ },
44
+ {
45
+ "day": "2026-05-17",
46
+ "users": 0
47
+ },
48
+ {
49
+ "day": "2026-05-18",
50
+ "users": 1
51
+ },
52
+ {
53
+ "day": "2026-05-19",
54
+ "users": 0
55
+ },
56
+ {
57
+ "day": "2026-05-20",
58
+ "users": 0
59
+ },
60
+ {
61
+ "day": "2026-05-21",
62
+ "users": 0
63
+ },
64
+ {
65
+ "day": "2026-05-22",
66
+ "users": 0
67
+ },
68
+ {
69
+ "day": "2026-05-23",
70
+ "users": 0
71
+ },
72
+ {
73
+ "day": "2026-05-24",
74
+ "users": 0
75
+ },
76
+ {
77
+ "day": "2026-05-25",
78
+ "users": 0
79
+ },
80
+ {
81
+ "day": "2026-05-26",
82
+ "users": 0
83
+ },
84
+ {
85
+ "day": "2026-05-27",
86
+ "users": 0
87
+ },
88
+ {
89
+ "day": "2026-05-28",
90
+ "users": 0
91
+ },
92
+ {
93
+ "day": "2026-05-29",
94
+ "users": 0
95
+ },
96
+ {
97
+ "day": "2026-05-30",
98
+ "users": 0
99
+ },
100
+ {
101
+ "day": "2026-05-31",
102
+ "users": 0
103
+ },
104
+ {
105
+ "day": "2026-06-01",
106
+ "users": 1
107
+ },
108
+ {
109
+ "day": "2026-06-02",
110
+ "users": 1
111
+ },
112
+ {
113
+ "day": "2026-06-03",
114
+ "users": 0
115
+ },
116
+ {
117
+ "day": "2026-06-04",
118
+ "users": 0
119
+ },
120
+ {
121
+ "day": "2026-06-05",
122
+ "users": 0
123
+ },
124
+ {
125
+ "day": "2026-06-06",
126
+ "users": 1
127
+ },
128
+ {
129
+ "day": "2026-06-07",
130
+ "users": 1
131
+ },
132
+ {
133
+ "day": "2026-06-08",
134
+ "users": 0
135
+ },
136
+ {
137
+ "day": "2026-06-09",
138
+ "users": 17
139
+ },
140
+ {
141
+ "day": "2026-06-10",
142
+ "users": 7
143
+ },
144
+ {
145
+ "day": "2026-06-11",
146
+ "users": 0
147
+ },
148
+ {
149
+ "day": "2026-06-12",
150
+ "users": 0
151
+ },
152
+ {
153
+ "day": "2026-06-13",
154
+ "users": 0
155
+ }
156
+ ],
157
+ "reports_by_day": [
158
+ {
159
+ "day": "2026-05-15",
160
+ "reports": 0
161
+ },
162
+ {
163
+ "day": "2026-05-16",
164
+ "reports": 0
165
+ },
166
+ {
167
+ "day": "2026-05-17",
168
+ "reports": 0
169
+ },
170
+ {
171
+ "day": "2026-05-18",
172
+ "reports": 0
173
+ },
174
+ {
175
+ "day": "2026-05-19",
176
+ "reports": 0
177
+ },
178
+ {
179
+ "day": "2026-05-20",
180
+ "reports": 0
181
+ },
182
+ {
183
+ "day": "2026-05-21",
184
+ "reports": 0
185
+ },
186
+ {
187
+ "day": "2026-05-22",
188
+ "reports": 0
189
+ },
190
+ {
191
+ "day": "2026-05-23",
192
+ "reports": 0
193
+ },
194
+ {
195
+ "day": "2026-05-24",
196
+ "reports": 0
197
+ },
198
+ {
199
+ "day": "2026-05-25",
200
+ "reports": 0
201
+ },
202
+ {
203
+ "day": "2026-05-26",
204
+ "reports": 0
205
+ },
206
+ {
207
+ "day": "2026-05-27",
208
+ "reports": 0
209
+ },
210
+ {
211
+ "day": "2026-05-28",
212
+ "reports": 0
213
+ },
214
+ {
215
+ "day": "2026-05-29",
216
+ "reports": 0
217
+ },
218
+ {
219
+ "day": "2026-05-30",
220
+ "reports": 0
221
+ },
222
+ {
223
+ "day": "2026-05-31",
224
+ "reports": 0
225
+ },
226
+ {
227
+ "day": "2026-06-01",
228
+ "reports": 2
229
+ },
230
+ {
231
+ "day": "2026-06-02",
232
+ "reports": 1
233
+ },
234
+ {
235
+ "day": "2026-06-03",
236
+ "reports": 3
237
+ },
238
+ {
239
+ "day": "2026-06-04",
240
+ "reports": 0
241
+ },
242
+ {
243
+ "day": "2026-06-05",
244
+ "reports": 0
245
+ },
246
+ {
247
+ "day": "2026-06-06",
248
+ "reports": 1
249
+ },
250
+ {
251
+ "day": "2026-06-07",
252
+ "reports": 1
253
+ },
254
+ {
255
+ "day": "2026-06-08",
256
+ "reports": 1
257
+ },
258
+ {
259
+ "day": "2026-06-09",
260
+ "reports": 51
261
+ },
262
+ {
263
+ "day": "2026-06-10",
264
+ "reports": 23
265
+ },
266
+ {
267
+ "day": "2026-06-11",
268
+ "reports": 0
269
+ },
270
+ {
271
+ "day": "2026-06-12",
272
+ "reports": 0
273
+ },
274
+ {
275
+ "day": "2026-06-13",
276
+ "reports": 0
277
+ }
278
+ ],
279
+ "users_by_language": [
280
+ {
281
+ "language": "Russian",
282
+ "users": 20
283
+ },
284
+ {
285
+ "language": "French",
286
+ "users": 8
287
+ },
288
+ {
289
+ "language": "English",
290
+ "users": 1
291
+ }
292
+ ],
293
+ "reports_by_type": [
294
+ {
295
+ "report_type": "Natal chart",
296
+ "language": "Russian",
297
+ "reports": 19
298
+ },
299
+ {
300
+ "report_type": "Life purpose",
301
+ "language": "Russian",
302
+ "reports": 9
303
+ },
304
+ {
305
+ "report_type": "Natal chart",
306
+ "language": "French",
307
+ "reports": 7
308
+ },
309
+ {
310
+ "report_type": "Career",
311
+ "language": "Russian",
312
+ "reports": 5
313
+ },
314
+ {
315
+ "report_type": "Finances",
316
+ "language": "Russian",
317
+ "reports": 5
318
+ },
319
+ {
320
+ "report_type": "karma",
321
+ "language": "Russian",
322
+ "reports": 5
323
+ },
324
+ {
325
+ "report_type": "Child chart",
326
+ "language": "Russian",
327
+ "reports": 4
328
+ },
329
+ {
330
+ "report_type": "Health",
331
+ "language": "Russian",
332
+ "reports": 4
333
+ },
334
+ {
335
+ "report_type": "Compatibility",
336
+ "language": "Russian",
337
+ "reports": 4
338
+ },
339
+ {
340
+ "report_type": "Career",
341
+ "language": "French",
342
+ "reports": 3
343
+ },
344
+ {
345
+ "report_type": "karma",
346
+ "language": "French",
347
+ "reports": 3
348
+ },
349
+ {
350
+ "report_type": "Life purpose",
351
+ "language": "French",
352
+ "reports": 3
353
+ },
354
+ {
355
+ "report_type": "Compatibility",
356
+ "language": "French",
357
+ "reports": 3
358
+ },
359
+ {
360
+ "report_type": "Finances",
361
+ "language": "French",
362
+ "reports": 2
363
+ },
364
+ {
365
+ "report_type": "Child: talents",
366
+ "language": "Russian",
367
+ "reports": 1
368
+ },
369
+ {
370
+ "report_type": "Forecast",
371
+ "language": "Russian",
372
+ "reports": 1
373
+ },
374
+ {
375
+ "report_type": "Health",
376
+ "language": "French",
377
+ "reports": 1
378
+ },
379
+ {
380
+ "report_type": "karma",
381
+ "language": "English",
382
+ "reports": 1
383
+ },
384
+ {
385
+ "report_type": "Natal chart",
386
+ "language": "English",
387
+ "reports": 1
388
+ },
389
+ {
390
+ "report_type": "Life purpose",
391
+ "language": "English",
392
+ "reports": 1
393
+ },
394
+ {
395
+ "report_type": "Relationships",
396
+ "language": "French",
397
+ "reports": 1
398
+ }
399
+ ],
400
+ "gpt_by_language": [
401
+ {
402
+ "language": "Russian",
403
+ "calls": 66,
404
+ "tokens": 427160
405
+ },
406
+ {
407
+ "language": "French",
408
+ "calls": 45,
409
+ "tokens": 337029
410
+ },
411
+ {
412
+ "language": "English",
413
+ "calls": 3,
414
+ "tokens": 15831
415
+ }
416
+ ],
417
+ "gpt_by_request_language": [
418
+ {
419
+ "request_type": "section",
420
+ "language": "Russian",
421
+ "calls": 35
422
+ },
423
+ {
424
+ "request_type": "dialog",
425
+ "language": "French",
426
+ "calls": 22
427
+ },
428
+ {
429
+ "request_type": "natal_chart",
430
+ "language": "Russian",
431
+ "calls": 19
432
+ },
433
+ {
434
+ "request_type": "section",
435
+ "language": "French",
436
+ "calls": 13
437
+ },
438
+ {
439
+ "request_type": "natal_chart",
440
+ "language": "French",
441
+ "calls": 7
442
+ },
443
+ {
444
+ "request_type": "child_chart",
445
+ "language": "Russian",
446
+ "calls": 4
447
+ },
448
+ {
449
+ "request_type": "forecast",
450
+ "language": "Russian",
451
+ "calls": 4
452
+ },
453
+ {
454
+ "request_type": "synastry",
455
+ "language": "French",
456
+ "calls": 3
457
+ },
458
+ {
459
+ "request_type": "synastry",
460
+ "language": "Russian",
461
+ "calls": 3
462
+ },
463
+ {
464
+ "request_type": "section",
465
+ "language": "English",
466
+ "calls": 2
467
+ },
468
+ {
469
+ "request_type": "child_talents",
470
+ "language": "Russian",
471
+ "calls": 1
472
+ },
473
+ {
474
+ "request_type": "natal_chart",
475
+ "language": "English",
476
+ "calls": 1
477
+ }
478
+ ]
479
+ }
480
+ }
requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
- altair
2
- pandas
3
- streamlit
 
1
+ streamlit>=1.45,<2
2
+ pandas==2.2.2
3
+ plotly==5.22.0