Spaces:
Running
Running
Download dashboard.py from Rasheed/Test_AI_writing: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/Rasheed/Test_AI_writing/resolve/main/dashboard.py
- Command line
-
hf download hf://spaces/Rasheed/Test_AI_writing/dashboard.py
-
curl -L -o dashboard.py https://huggingface.co/spaces/Rasheed/Test_AI_writing/resolve/main/dashboard.py
15.6 kB
| """Run history + user dashboard + admin analytics (SQLite, same DB as accounts).""" | |
| import csv | |
| import html | |
| import os | |
| import tempfile | |
| import time | |
| from pathlib import Path | |
| import auth as A | |
| import db as _dbmod | |
| # every tool that writes to the run history (charts, the admin's tool filter and the tool table follow this list) | |
| TOOLS = ["Smart Humanize", "AI Detector", "Humanizer", "Proofread", "Journal Formatter", "Journal Finder"] | |
| TOOL_COLORS = {"Smart Humanize": "#47148B", "AI Detector": "#50B4EF", "Humanizer": "#E92E7F", "Grammar": "#50BF49", | |
| "Proofread": "#50BF49", "Journal Formatter": "#FE5828", "Journal Finder": "#F5C518"} | |
| SEARCH_TOOLS = ("Journal Finder",) # runs that are a search, not a text or a document | |
| RETENTION_DAYS = int(os.getenv("HISTORY_DAYS", "180")) | |
| def _init(con): | |
| if _dbmod.IS_MYSQL: | |
| return con | |
| con.execute("""CREATE TABLE IF NOT EXISTS runs ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT DEFAULT CURRENT_TIMESTAMP, who TEXT, email TEXT, | |
| tool TEXT, kind TEXT, name TEXT, words INTEGER, ai_before INTEGER, ai_after INTEGER, | |
| secs REAL, status TEXT, output TEXT, engine TEXT)""") | |
| con.execute("CREATE INDEX IF NOT EXISTS runs_email ON runs(email, ts)") | |
| con.execute("CREATE INDEX IF NOT EXISTS runs_ts ON runs(ts)") | |
| return con | |
| def db(): | |
| return _init(A._db()) | |
| # ---------------------------------------------------------------- logging | |
| def log_run(user, request, tool, document, meta, secs, status, error=""): | |
| try: | |
| with db() as con: | |
| con.execute( | |
| "INSERT INTO runs (who, email, tool, kind, name, words, ai_before, ai_after, secs, status, " | |
| "output, engine) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", | |
| (A._who(user, request), (user or {}).get("email"), tool, "doc" if document else "text", | |
| (meta.get("name") or ("Pasted text" if not document else ""))[:120], meta.get("words"), | |
| meta.get("before"), meta.get("after"), round(secs, 1), | |
| status if not error else "error: " + error.strip("\x27\" ")[:150], meta.get("output"), meta.get("engine"))) | |
| con.execute(f"DELETE FROM runs WHERE ts < datetime('now','-{RETENTION_DAYS} days')") | |
| except Exception as err: | |
| print(f"[dashboard] log failed: {err}") | |
| # ---------------------------------------------------------------- helpers | |
| def _tiles(items): | |
| cells = "".join(f'<div class="cs-stat"><div class="n">{v}</div><div class="l">{k}</div></div>' | |
| for k, v in items) | |
| return f'<div class="cs-stats">{cells}</div>' | |
| def _fmt_pct(v): | |
| return "—" if v is None else f"{v}%" | |
| def _daily(con, where, params, days): | |
| """Rows for a stacked bar chart: one row per (day, tool) with zero-filled days.""" | |
| import pandas as pd | |
| rows = con.execute( | |
| f"SELECT date(ts) d, tool, COUNT(*) FROM runs WHERE ts >= date('now', ?) {where} GROUP BY d, tool", | |
| (f"-{days - 1} days", *params)).fetchall() | |
| got = {(d, t): n for d, t, n in rows} | |
| today = time.gmtime() | |
| out = [] | |
| for i in range(days - 1, -1, -1): | |
| d = time.strftime("%Y-%m-%d", time.gmtime(time.mktime(today) - i * 86400)) | |
| for t in TOOLS: | |
| out.append({"Day": d[5:], "Tool": t, "Runs": got.get((d, t), 0)}) | |
| return pd.DataFrame(out) | |
| HIST_COLS = ["When (UTC)", "Tool", "Input", "Type", "Words", "AI before", "AI after", "Time (s)", "Status"] | |
| def _clean(t): | |
| import privacy | |
| return privacy.clean(t) | |
| def _hist_rows(rows): | |
| return [[(ts or "")[:16], tool, name or "—", | |
| "Search" if tool in SEARCH_TOOLS else "Document" if kind == "doc" else "Text", | |
| words if words is not None else "—", _fmt_pct(b), _fmt_pct(a), secs, | |
| "✅ OK" if status == "ok" else "⚠️ " + (_clean((status or "")[7:200]) or "Could not finish")[:80]] | |
| for ts, tool, name, kind, words, b, a, secs, status in rows] | |
| # ---------------------------------------------------------------- pay per use (plans.charges) | |
| def _charges(where="", params=()): | |
| """Rows of the pay-per-use charges table ([] when it does not exist yet or cannot be read).""" | |
| try: | |
| import plans as P | |
| with P._db() as con: | |
| return con.execute("SELECT ref, ts, email, tool, name, words, cost_usd, amount_inr, status, paid_ts " | |
| f"FROM charges {where} ORDER BY id DESC", params).fetchall() | |
| except Exception as err: | |
| print(f"[dashboard] charges not read: {err}") | |
| return [] | |
| def _pending_html(email): | |
| """Results of this customer that are waiting for payment, each with its price, reference and a Pay link.""" | |
| rows = _charges("WHERE email=? AND status='pending'", ((email or "").lower(),))[:5] | |
| if not rows: | |
| return "" | |
| import plans as P | |
| items = [] | |
| for ref, ts, em, tool, name, words, cost, inr, status, paid in rows: | |
| link = html.escape(P.pay_link(dict(ref=ref, amount_inr=inr or 0, tool=tool, name=name, email=em)), quote=True) | |
| items.append(f'<div class="cs-nudge">🔒 <b>{html.escape(str(name or "Document"))}</b> ({html.escape(tool or "")}, ' | |
| f'{(ts or "")[:10]}) is waiting for payment: <b>₹{(inr or 0):,.0f}</b>, reference ' | |
| f'<b>{html.escape(ref)}</b>. <a class="cs-up-link" href="{link}" target="_blank" ' | |
| f'rel="noopener">Pay now</a> — then open the tool\'s tab and press “I have paid: unlock”.</div>') | |
| return "".join(items) | |
| # ---------------------------------------------------------------- user dashboard | |
| def user_dashboard(user): | |
| """(tiles_html, chart_df, history_rows, file_choices)""" | |
| import pandas as pd | |
| if not user: | |
| return ("", pd.DataFrame(columns=["Day", "Tool", "Runs"]), [], []) | |
| e = user["email"] | |
| with db() as con: | |
| tot, docs, texts, ok = con.execute( | |
| "SELECT COUNT(*), SUM(kind='doc'), SUM(kind='text' AND tool!='Journal Finder'), SUM(status='ok') " | |
| "FROM runs WHERE email=?", (e,)).fetchone() | |
| formatted, searches, proofs = con.execute( | |
| "SELECT SUM(tool='Journal Formatter' AND status='ok'), SUM(tool='Journal Finder' AND status='ok'), " | |
| "SUM(tool='Proofread' AND status='ok') FROM runs WHERE email=?", (e,)).fetchone() | |
| red = con.execute( | |
| "SELECT AVG(ai_before - ai_after), SUM(words) FROM runs WHERE email=? AND status='ok' " | |
| "AND ai_before IS NOT NULL AND ai_after IS NOT NULL", (e,)).fetchone() | |
| words_all = con.execute("SELECT COALESCE(SUM(words),0) FROM runs WHERE email=? AND status='ok'", | |
| (e,)).fetchone()[0] | |
| chart = _daily(con, "AND email=?", (e,), 14) | |
| hist = con.execute("SELECT ts, tool, name, kind, words, ai_before, ai_after, secs, status FROM runs " | |
| "WHERE email=? ORDER BY ts DESC, id DESC LIMIT 100", (e,)).fetchall() | |
| files = con.execute("SELECT id, ts, tool, name, output FROM runs WHERE email=? AND status='ok' " | |
| "AND output IS NOT NULL ORDER BY id DESC LIMIT 40", (e,)).fetchall() | |
| who = A._who(user, None) | |
| lim = A.user_limits(e) | |
| used = [] | |
| for kind, label in (("doc", "docs"), ("text", "texts")): | |
| n = A.usage_today(who, kind) | |
| used.append(f"{n}/{lim[kind] if lim[kind] > 0 else '∞'} {label}") | |
| tiles = _tiles([ | |
| ("Total runs", f"{tot or 0:,}"), | |
| ("Documents", f"{docs or 0:,}"), | |
| ("Pasted texts", f"{texts or 0:,}"), | |
| ("Words processed", f"{words_all or 0:,}"), | |
| ("Avg AI reduction", "—" if red[0] is None else f"{red[0]:.0f} pts"), | |
| ("Proofreads", f"{proofs or 0:,}"), | |
| ("Papers formatted", f"{formatted or 0:,}"), | |
| ("Journal searches", f"{searches or 0:,}"), | |
| ("Used today", " · ".join(used) if not A.is_admin(user) else "Unlimited"), | |
| ]) + _pending_html(e) | |
| choices = [(f"#{i} · {ts[:16]} · {tool} · {name or 'Pasted text'}", str(i)) | |
| for i, ts, tool, name, out in files if out and Path(out).exists()] | |
| return tiles, chart, _hist_rows(hist), choices | |
| def user_file(user, run_id): | |
| if not user or not run_id: | |
| return None | |
| with db() as con: | |
| row = con.execute("SELECT output FROM runs WHERE id=? AND email=?", (int(run_id), user["email"])).fetchone() | |
| return row[0] if row and row[0] and Path(row[0]).exists() else None | |
| # ---------------------------------------------------------------- admin analytics | |
| def admin_overview(user): | |
| """(tiles_html, daily_chart_df, tool_df, top_users_rows)""" | |
| A._need_admin(user) | |
| import pandas as pd | |
| with db() as con: | |
| users = con.execute("SELECT COUNT(*), SUM(active=1), SUM(created >= datetime('now','-7 days')) " | |
| "FROM users").fetchone() | |
| today = con.execute("SELECT COUNT(*), SUM(email IS NULL), SUM(status!='ok') FROM runs " | |
| "WHERE date(ts)=date('now')").fetchone() | |
| week = con.execute("SELECT COUNT(*), COUNT(DISTINCT who), SUM(status!='ok'), " | |
| "AVG(CASE WHEN status='ok' THEN secs END) FROM runs " | |
| "WHERE ts >= datetime('now','-7 days')").fetchone() | |
| red = con.execute("SELECT AVG(ai_before - ai_after) FROM runs WHERE status='ok' AND tool!='AI Detector' " | |
| "AND ai_before IS NOT NULL AND ai_after IS NOT NULL " | |
| "AND ts >= datetime('now','-30 days')").fetchone()[0] | |
| cpu = con.execute("SELECT COUNT(*) FROM runs WHERE engine='CPU' AND ts >= datetime('now','-7 days')" | |
| ).fetchone()[0] | |
| chart = _daily(con, "", (), 30) | |
| tools = con.execute("SELECT tool, kind, COUNT(*) FROM runs WHERE ts >= datetime('now','-30 days') " | |
| "GROUP BY tool, kind").fetchall() | |
| top = con.execute( | |
| "SELECT r.email, u.name, COUNT(*), SUM(r.kind='doc'), COALESCE(SUM(r.words),0), MAX(r.ts) FROM runs r " | |
| "LEFT JOIN users u ON u.email=r.email WHERE r.email IS NOT NULL AND r.ts >= datetime('now','-30 days') " | |
| "GROUP BY r.email ORDER BY COUNT(*) DESC LIMIT 15").fetchall() | |
| by_tool = {} | |
| for t, k, n in tools: | |
| by_tool[t] = by_tool.get(t, 0) + n | |
| pending = _charges("WHERE status='pending'") | |
| paid = _charges("WHERE status='paid' AND paid_ts >= datetime('now','-30 days')") | |
| tiles = _tiles([ | |
| ("Users", f"{users[0] or 0:,}"), ("Active accounts", f"{users[1] or 0:,}"), | |
| ("New users (7d)", f"{users[2] or 0:,}"), ("Runs today", f"{today[0] or 0:,}"), | |
| ("Guest runs today", f"{today[1] or 0:,}"), ("Runs (7d)", f"{week[0] or 0:,}"), | |
| ("Unique visitors (7d)", f"{week[1] or 0:,}"), ("Errors (7d)", f"{week[2] or 0:,}"), | |
| ("Avg job time (7d)", "—" if week[3] is None else f"{week[3]:.0f}s"), | |
| ("Avg AI reduction (30d)", "—" if red is None else f"{red:.0f} pts"), | |
| ("Runs on CPU (7d)", f"{cpu:,}"), | |
| ("Papers formatted (30d)", f"{by_tool.get('Journal Formatter', 0):,}"), | |
| ("Journal searches (30d)", f"{by_tool.get('Journal Finder', 0):,}"), | |
| ("Proofreads (30d)", f"{by_tool.get('Proofread', 0):,}"), | |
| ("Pay-per-use awaiting payment", f"{len(pending):,} · ₹{sum(r[7] or 0 for r in pending):,.0f}"), | |
| ("Pay-per-use paid (30d)", f"{len(paid):,} · ₹{sum(r[7] or 0 for r in paid):,.0f}"), | |
| ("Pay-per-use cost (30d)", f"${sum(r[6] or 0 for r in paid):,.2f}"), | |
| ]) | |
| tdf = pd.DataFrame([{"Tool": t, "Type": "Search" if t in SEARCH_TOOLS else "Document" if k == "doc" else "Text", | |
| "Runs": n} for t, k, n in tools] or [{"Tool": t, "Type": "Text", "Runs": 0} for t in TOOLS]) | |
| top_rows = [[name or "—", e, n, d or 0, f"{w:,}", (last or "")[:16]] for e, name, n, d, w, last in top] | |
| return tiles, chart, tdf, top_rows | |
| ACT_COLS = ["When (UTC)", "User", "Tool", "Input", "Type", "Words", "AI before", "AI after", "Time (s)", | |
| "Engine", "Status"] | |
| def _mask(who, email): | |
| if email: | |
| return email | |
| ip = (who or "")[3:] | |
| parts = ip.split(".") | |
| return "guest " + (".".join(parts[:3]) + ".x" if len(parts) == 4 else ip[:12] + "…") | |
| def admin_activity(user, query="", tool="All", status="All", limit=300): | |
| A._need_admin(user) | |
| where, params = ["1=1"], [] | |
| q = (query or "").strip().lower() | |
| if q: | |
| where.append("(lower(COALESCE(email,'')) LIKE ? OR lower(COALESCE(name,'')) LIKE ? OR who LIKE ?)") | |
| params += [f"%{q}%"] * 3 | |
| if tool and tool != "All": | |
| where.append("tool=?") | |
| params.append(tool) | |
| if status == "Errors only": | |
| where.append("status!='ok'") | |
| elif status == "Guests only": | |
| where.append("email IS NULL") | |
| with db() as con: | |
| rows = con.execute( | |
| f"SELECT ts, who, email, tool, name, kind, words, ai_before, ai_after, secs, engine, status FROM runs " | |
| f"WHERE {' AND '.join(where)} ORDER BY ts DESC, id DESC LIMIT {int(limit)}", params).fetchall() | |
| return [[(ts or "")[:16], _mask(who, e), t, n or "—", | |
| "Search" if t in SEARCH_TOOLS else "Document" if k == "doc" else "Text", | |
| w if w is not None else "—", _fmt_pct(b), _fmt_pct(a), s, eng or "—", | |
| "✅ OK" if st == "ok" else "⚠️ " + (st or "")[7:80]] | |
| for ts, who, e, t, n, k, w, b, a, s, eng, st in rows] | |
| def admin_export_activity(user): | |
| A._need_admin(user) | |
| folder = Path(tempfile.mkdtemp(dir=A.OUT_DIR)) | |
| path = folder / "activity.csv" | |
| with open(path, "w", newline="", encoding="utf-8") as f: | |
| w = csv.writer(f) | |
| w.writerow(ACT_COLS) | |
| w.writerows(admin_activity(user, limit=100000)) | |
| return str(path) | |
| def admin_user_detail(user, email): | |
| """(summary_html, history_rows) for one user.""" | |
| A._need_admin(user) | |
| email = (email or "").strip().lower() | |
| if not email: | |
| return '<div class="cs-authmsg cs-bad">Enter a user email.</div>', [] | |
| with db() as con: | |
| u = con.execute("SELECT name, created, last_login, active, doc_limit, text_limit FROM users WHERE email=?", | |
| (email,)).fetchone() | |
| if not u: | |
| return f'<div class="cs-authmsg cs-bad">No user found with {html.escape(email)}.</div>', [] | |
| s = con.execute("SELECT COUNT(*), SUM(kind='doc'), SUM(kind='text'), COALESCE(SUM(words),0), " | |
| "SUM(status!='ok') FROM runs WHERE email=?", (email,)).fetchone() | |
| hist = con.execute("SELECT ts, tool, name, kind, words, ai_before, ai_after, secs, status FROM runs " | |
| "WHERE email=? ORDER BY ts DESC, id DESC LIMIT 200", (email,)).fetchall() | |
| lim = A.user_limits(email) | |
| fmt = lambda v, custom: ("∞" if v <= 0 else str(v)) + (" (custom)" if custom is not None else "") | |
| summary = (f'<div class="cs-auth-title" style="text-align:left">{html.escape(u[0] or "")} ' | |
| f'<small style="color:#888;font-weight:500">{html.escape(email)}</small></div>' | |
| + _tiles([("Joined", (u[1] or "")[:10]), ("Last login", (u[2] or "")[:16]), | |
| ("Status", "Active" if u[3] else "Disabled"), ("Runs", f"{s[0] or 0:,}"), | |
| ("Docs / texts", f"{s[1] or 0} / {s[2] or 0}"), ("Words", f"{s[3]:,}"), | |
| ("Errors", f"{s[4] or 0}"), | |
| ("Daily limits (doc / text)", f"{fmt(lim['doc'], u[4])} / {fmt(lim['text'], u[5])}")])) | |
| return summary, _hist_rows(hist) | |