diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 775f3e3..f0018a1 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,11 @@ "Bash(docker compose up -d)", "Bash(docker exec mastodon-collector-collector-1 bash -c \"ANALYZER_LIMIT=100 python -m app.analyzer\")", "Bash(docker compose build collector)", - "Bash(docker compose up -d collector)" + "Bash(docker compose up -d collector)", + "Bash(docker compose build web)", + "Bash(docker compose up -d web)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8585/analysis)", + "Bash(docker logs mastodon-collector-web-1 --tail 30)" ], "deny": [], "ask": [] diff --git a/.gitignore b/.gitignore index 595ac22..628546d 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ venv.bak/ *.swo *~ .DS_Store +.claude/ # Database files *.sqlite diff --git a/app/analysis_helpers.py b/app/analysis_helpers.py index 5dde7c7..c205e5b 100644 --- a/app/analysis_helpers.py +++ b/app/analysis_helpers.py @@ -17,21 +17,40 @@ def get_analysis_stats(session: Session) -> dict: """Get overall toxicity analysis statistics.""" from sqlalchemy import text - # Total statuses and scored statuses - total_statuses = session.query(func.count(Status.id)).scalar() or 0 + # Total statuses by type + total_posts = session.query(func.count(Status.id)).filter(Status.status_type == 'post').scalar() or 0 + total_replies = session.query(func.count(Status.id)).filter(Status.status_type == 'reply').scalar() or 0 + total_mentions = session.query(func.count(Status.id)).filter(Status.status_type == 'mention').scalar() or 0 - scored = session.execute(text(""" - SELECT COUNT(*) as total_scored, - COUNT(*) FILTER (WHERE flagged = true) as flagged, - AVG(overall) as avg_toxicity - FROM toxicity_scores + # Scored statuses by type + scored_stats = session.execute(text(""" + SELECT + COUNT(*) FILTER (WHERE s.status_type = 'post') as scored_posts, + COUNT(*) FILTER (WHERE s.status_type = 'reply') as scored_replies, + COUNT(*) FILTER (WHERE s.status_type = 'mention') as scored_mentions, + COUNT(*) FILTER (WHERE ts.flagged = true AND s.status_type = 'post') as flagged_posts, + COUNT(*) FILTER (WHERE ts.flagged = true AND s.status_type = 'reply') as flagged_replies, + COUNT(*) FILTER (WHERE ts.flagged = true AND s.status_type = 'mention') as flagged_mentions, + AVG(ts.overall) FILTER (WHERE s.status_type = 'post') as avg_toxicity_posts, + AVG(ts.overall) FILTER (WHERE s.status_type = 'reply') as avg_toxicity_replies, + AVG(ts.overall) FILTER (WHERE s.status_type = 'mention') as avg_toxicity_mentions + FROM toxicity_scores ts + JOIN statuses s ON s.id = ts.status_id """)).fetchone() return { - "total_statuses": total_statuses, - "total_scored_statuses": scored[0] if scored else 0, - "flagged_statuses": scored[1] if scored else 0, - "avg_toxicity_statuses": float(scored[2]) if scored and scored[2] else 0.0, + "total_posts": total_posts, + "total_replies": total_replies, + "total_mentions": total_mentions, + "total_scored_posts": scored_stats[0] if scored_stats else 0, + "total_scored_replies": scored_stats[1] if scored_stats else 0, + "total_scored_mentions": scored_stats[2] if scored_stats else 0, + "flagged_posts": scored_stats[3] if scored_stats else 0, + "flagged_replies": scored_stats[4] if scored_stats else 0, + "flagged_mentions": scored_stats[5] if scored_stats else 0, + "avg_toxicity_posts": float(scored_stats[6]) if scored_stats and scored_stats[6] else 0.0, + "avg_toxicity_replies": float(scored_stats[7]) if scored_stats and scored_stats[7] else 0.0, + "avg_toxicity_mentions": float(scored_stats[8]) if scored_stats and scored_stats[8] else 0.0, } @@ -39,17 +58,17 @@ def get_toxicity_trend(session: Session, weeks: int = 12) -> list[dict]: """Get toxicity trend over time (weekly aggregates).""" from sqlalchemy import text - result = session.execute(text(""" + result = session.execute(text(f""" SELECT DATE_TRUNC('week', s.created_at) as week, AVG(ts.overall) as avg_toxicity, COUNT(*) FILTER (WHERE ts.flagged = true) as flagged_count FROM statuses s JOIN toxicity_scores ts ON ts.status_id = s.id - WHERE s.created_at >= NOW() - INTERVAL ':weeks weeks' + WHERE s.created_at >= NOW() - INTERVAL '{weeks} weeks' GROUP BY week ORDER BY week DESC - """), {"weeks": weeks}) + """)) return [{"week": r[0], "avg_toxicity": float(r[1]) if r[1] else 0.0, "flagged_statuses": r[2]} for r in result] @@ -98,7 +117,8 @@ def get_recent_analysis_runs(session: Session, limit: int = 5) -> list[dict]: "started_at": r[1], "finished_at": r[2], "status": r[3], - "statuses_scored": r[4], + "posts_scored": r[4], # Template expects posts_scored + "mentions_scored": 0, # Not tracked separately in Mastodon "errors": r[5], "cost_usd": r[6], "duration_secs": r[7] diff --git a/app/templates/analysis.html b/app/templates/analysis.html index 1f8da90..47d89dd 100644 --- a/app/templates/analysis.html +++ b/app/templates/analysis.html @@ -423,8 +423,8 @@
diff --git a/app/templates/flagged.html b/app/templates/flagged.html index ec8ca2c..d7edc43 100644 --- a/app/templates/flagged.html +++ b/app/templates/flagged.html @@ -12,7 +12,7 @@