Fix toxicity analysis web interface
- Fix analysis_helpers stats to match template expectations (posts/replies/mentions breakdown) - Fix SQL interval syntax in trend query - Fix URL routing in templates (analysis_flagged, accounts_list) - Add .claude/ to .gitignore Analysis dashboard now accessible at http://localhost:8585/analysis 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a8b28f63a0
commit
2172efa701
5 changed files with 49 additions and 24 deletions
|
|
@ -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": []
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -46,6 +46,7 @@ venv.bak/
|
|||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
.claude/
|
||||
|
||||
# Database files
|
||||
*.sqlite
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -423,8 +423,8 @@
|
|||
<!-- Quick Links -->
|
||||
<div style="margin-top: 2rem;">
|
||||
<div class="quick-links">
|
||||
<a href="{{ url_for('analysis.flagged') }}" class="quick-link">View Flagged Content</a>
|
||||
<a href="{{ url_for('analysis.accounts') }}" class="quick-link">Account Breakdown</a>
|
||||
<a href="{{ url_for('analysis_flagged') }}" class="quick-link">View Flagged Content</a>
|
||||
<a href="{{ url_for('accounts_list') }}" class="quick-link">Account Breakdown</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<!-- Filter Bar -->
|
||||
<div class="filter-bar">
|
||||
<form method="get" action="{{ url_for('analysis.flagged') }}" class="filter-form">
|
||||
<form method="get" action="{{ url_for('analysis_flagged') }}" class="filter-form">
|
||||
<div class="filter-group">
|
||||
<label for="content-type">Type:</label>
|
||||
<select id="content-type" name="content_type" class="filter-select">
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
|
||||
{% macro sort_header(col, label) %}
|
||||
{% set new_dir = 'asc' if (sort == col and direction == 'desc') else 'desc' %}
|
||||
<a href="{{ url_for('analysis.flagged', content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=col, dir=new_dir) }}" class="sort-link">
|
||||
<a href="{{ url_for('analysis_flagged', content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=col, dir=new_dir) }}" class="sort-link">
|
||||
{{ label }}
|
||||
{% if sort == col %}
|
||||
<span class="sort-icon">{{ '▼' if direction == 'desc' else '▲' }}</span>
|
||||
|
|
@ -201,15 +201,15 @@
|
|||
{% if total_pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="{{ url_for('analysis.flagged', page=1, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">First</a>
|
||||
<a href="{{ url_for('analysis.flagged', page=page-1, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">Previous</a>
|
||||
<a href="{{ url_for('analysis_flagged', page=1, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">First</a>
|
||||
<a href="{{ url_for('analysis_flagged', page=page-1, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">Previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="pagination-info">Page {{ page }} of {{ total_pages }}</span>
|
||||
|
||||
{% if page < total_pages %}
|
||||
<a href="{{ url_for('analysis.flagged', page=page+1, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">Next</a>
|
||||
<a href="{{ url_for('analysis.flagged', page=total_pages, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">Last</a>
|
||||
<a href="{{ url_for('analysis_flagged', page=page+1, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">Next</a>
|
||||
<a href="{{ url_for('analysis_flagged', page=total_pages, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">Last</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue