diff --git a/web/app.py b/web/app.py index 1312dd9..87c3e9c 100644 --- a/web/app.py +++ b/web/app.py @@ -21,6 +21,7 @@ from web.db import ( set_user_keywords, get_all_regions, get_all_keywords, + stats_overview, upsert_region, upsert_keyword, list_regions_full, @@ -487,6 +488,34 @@ def admin_taxonomy(): return render_template('admin/taxonomy.html', title='Taxonomy', regions=regions, keywords=keywords) +@app.route('/admin/stats', methods=['GET']) +def admin_stats(): + if not require_admin(): + return redirect(url_for('login')) + # Optional filters via query params + keyword = request.args.get('keyword') + region = request.args.get('region') + try: + stats = stats_overview() + # For detailed jobs table, reuse get_all_jobs() and filter + jobs = get_all_jobs() + if keyword: + jobs = [j for j in jobs if (j.get('keyword') or '') == keyword] + if region: + jobs = [j for j in jobs if (j.get('region') or '') == region] + except Exception as e: + flash(f'Error computing stats: {e}') + stats = { + 'total_jobs': 0, + 'total_keywords': 0, + 'total_regions': 0, + 'jobs_per_keyword': [], + 'jobs_per_region': [] + } + jobs = [] + return render_template('admin/stats.html', title='Statistics', stats=stats, jobs=jobs, regions=get_all_regions(), keywords=get_all_keywords()) + + def init(): """Main function to run the Flask app.""" # Ensure DB is initialized diff --git a/web/db.py b/web/db.py index 0b39058..37adca6 100644 --- a/web/db.py +++ b/web/db.py @@ -783,3 +783,44 @@ def change_keyword_color(keyword_id: int, new_color: str) -> bool: except Exception: session.rollback() return False + + +def stats_overview() -> Dict[str, Any]: + """Return an overview of job DB statistics. + + Returns a dict with keys: + - total_jobs: int + - total_keywords: int (distinct keywords in listings) + - total_regions: int (distinct regions in listings) + - jobs_per_keyword: List[{"keyword": str, "count": int}] + - jobs_per_region: List[{"region": str, "count": int}] + """ + with _ensure_session() as session: + total_jobs = session.execute(text( + "SELECT COUNT(*) FROM job_listings l INNER JOIN job_descriptions d ON l.job_id = d.job_id AND l.url = d.url" + )).scalar_one() + total_keywords = session.execute(text( + "SELECT COUNT(DISTINCT keyword) FROM job_listings WHERE keyword IS NOT NULL AND keyword != ''" + )).scalar_one() + total_regions = session.execute(text( + "SELECT COUNT(DISTINCT region) FROM job_listings WHERE region IS NOT NULL AND region != ''" + )).scalar_one() + + rows = session.execute(text( + "SELECT COALESCE(keyword, '') AS keyword, COUNT(*) as cnt FROM job_listings l INNER JOIN job_descriptions d ON l.job_id = d.job_id AND l.url = d.url GROUP BY keyword ORDER BY cnt DESC" + )).fetchall() + jobs_per_keyword = [ + {"keyword": r[0], "count": int(r[1])} for r in rows] + + rows = session.execute(text( + "SELECT COALESCE(region, '') AS region, COUNT(*) as cnt FROM job_listings l INNER JOIN job_descriptions d ON l.job_id = d.job_id AND l.url = d.url GROUP BY region ORDER BY cnt DESC" + )).fetchall() + jobs_per_region = [{"region": r[0], "count": int(r[1])} for r in rows] + + return { + "total_jobs": int(total_jobs or 0), + "total_keywords": int(total_keywords or 0), + "total_regions": int(total_regions or 0), + "jobs_per_keyword": jobs_per_keyword, + "jobs_per_region": jobs_per_region, + } diff --git a/web/templates/admin/stats.html b/web/templates/admin/stats.html new file mode 100644 index 0000000..d773f93 --- /dev/null +++ b/web/templates/admin/stats.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} {% block content %} +
Total jobs: {{ stats.total_jobs }}
+Total keywords: {{ stats.total_keywords }}
+Total regions: {{ stats.total_regions }}
+| Keyword | +Count | +
|---|---|
| {{ row.keyword or '(empty)' }} | +{{ row.count }} | +
| Region | +Count | +
|---|---|
| {{ row.region or '(empty)' }} | +{{ row.count }} | +