Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class MyAttack(ResearchAttackStrategy):
```
┌──────────────────────────────────────────────────────────────┐
│ React + Vite Frontend │
(Leaderboard · Arena · Bench · Detail)
(Dashboard · Arena · Bench · Leaderboard · Jobs · Detail)
└───────────────────────────┬──────────────────────────────────┘
│ REST + polling
┌───────────────────────────▼──────────────────────────────────┐
Expand Down Expand Up @@ -201,7 +201,7 @@ fedarena/
│ │ │ └── config.py # Pydantic settings (.env loading)
│ │ └── runners/ # FL runtime (core_runtime.py)
│ └── frontend/ # React + Vite + Tailwind + Radix UI
│ └── src/pages/ # Leaderboard, Arena, Bench, Detail
│ └── src/pages/ # Dashboard, Arena, Bench, Leaderboard, Jobs, Detail
├── libs/fl_core/ # FL core library
│ ├── research/ # Arena engine (registry, runner, arena, base classes)
│ │ ├── attacks/ # Baseline + user submissions
Expand Down Expand Up @@ -234,21 +234,21 @@ fedarena/

### Phase 2: Range / Scenario System

- [ ] Scenario library — multiple datasets, non-IID levels, malicious client ratios, model architectures
- [ ] Multi-metric scoring — Attack Success Rate, Accuracy Drop, Convergence Speed, Runtime Cost, confidence intervals
- [ ] Multi-dimensional leaderboard — Overall Score, Effectiveness, Robustness, Stability, Runtime columns
- [ ] Per-scenario leaderboards
- [ ] Sandbox execution — container isolation, timeout, network & filesystem restrictions for user-submitted code
- [ ] Method versioning — track iterations of the same attack/defense, side-by-side comparison
- [ ] Experiment comparison — cross-run overlay charts, statistical significance tests
- [ ] Analytical reports — LLM-generated strengths/weaknesses, failure analysis, baseline diff, next-step recommendations
- [ ] Dashboard page — live arena status, active jobs, top methods, recent submissions
- [ ] Navigation restructure — Dashboard / Scenarios / Arena / Bench / Leaderboard / Reports / Methods / Jobs
- [ ] Arena UX improvements — left/right layout, template mode, evaluation intensity selection
- [ ] Matrix interaction — filter by scenario, click cell for run details & training curves
- [x] Scenario library — multiple datasets, non-IID levels, malicious client ratios, model architectures
- [x] Multi-metric scoring — Accuracy Drop, Convergence Speed, Stability, Runtime Cost, Max Accuracy, per-opponent and summary aggregation
- [x] Multi-dimensional leaderboard — Avg Accuracy, Accuracy Drop, Worst Case, Convergence, Stability columns with sort_by support
- [x] Per-scenario leaderboards
- [x] Method versioning — track iterations of the same attack/defense, side-by-side comparison
- [x] Experiment comparison — cross-run overlay charts, side-by-side metrics
- [x] Analytical reports — LLM-generated strengths/weaknesses, baseline comparison, recommendations, cached per evaluation
- [x] Dashboard page — live arena status, active jobs, top methods, recent submissions
- [x] Navigation restructure — Dashboard / Scenarios / Arena / Bench / Leaderboard / Reports / Methods / Jobs
- [x] Arena UX improvements — left/right layout, template mode, evaluation intensity selection
- [x] Matrix interaction — filter by scenario, click cell for run details & per-seed breakdown

### Phase 3: Platform

- [ ] Sandbox execution — container isolation, timeout, network & filesystem restrictions for user-submitted code
- [ ] User & team accounts with permissions
- [ ] Challenge mode — fixed scenarios, time-limited competitions, hidden test sets
- [ ] Course mode — guided exercises for FL security education
Expand Down
7 changes: 3 additions & 4 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,12 @@ fedarena/

欢迎贡献!FedArena 致力于成为简洁、可扩展的联邦学习安全研究平台。

- [ ] **多配置矩阵** — 支持多数据集 / IID / 客户端数配置,拓宽评测覆盖面
- [ ] **Markdown 报告生成** — 可导出的逐提交对比报告
- [x] **多配置矩阵** — 场景库:多数据集 / non-IID 级别 / 恶意比例 / 模型架构
- [x] **Markdown 报告生成** — 可导出的逐提交对比报告(Markdown + PDF)
- [ ] **用户认证** — 简单的账号系统,关联提交归属
- [ ] **Docker 部署** — 一键 `docker-compose` 启动后端 + 前端 + GPU
- [ ] **更多攻防方法** — 数据投毒、自适应裁剪、几何中位数等
- [ ] **可复现性保障** — 固定种子、配置哈希校验、环境指纹
- [ ] **CI 流水线** — 自动化测试:注册表发现、提交校验、API 端点
- [x] **CI 流水线** — 自动化测试:注册表发现、提交校验、API 端点

<p align="center">
<sub>FedArena 仅供研究和教学用途。</sub>
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/app/api/v1/bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class BenchJobResponse(BaseModel):
error: str | None
started_at: str | None
completed_at: str | None
created_at: str | None


def _build_response(job: BenchJob) -> BenchJobResponse:
Expand All @@ -64,6 +65,7 @@ def _build_response(job: BenchJob) -> BenchJobResponse:
error=job.error,
started_at=str(job.started_at) if job.started_at else None,
completed_at=str(job.completed_at) if job.completed_at else None,
created_at=str(job.bench_created_at),
)


Expand Down
135 changes: 135 additions & 0 deletions apps/backend/app/api/v1/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Dashboard aggregation endpoint."""

from __future__ import annotations

from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import func, select

from ...db import get_session
from ...models import BenchJob, EvaluationJob, Submission
from ...services.task_queue import get_queue

router = APIRouter(prefix="/dashboard", tags=["dashboard"])


class TopMethod(BaseModel):
submission_id: int
display_name: str
method_name: str
author: str | None
avg_accuracy: float


class RecentSubmission(BaseModel):
id: int
display_name: str
method_name: str
role: str
status: str
created_at: str

model_config = {"from_attributes": True}


class DashboardResponse(BaseModel):
total_submissions: int
completed_evaluations: int
failed_evaluations: int
active_jobs: int
queue_pending: int
top_attack: TopMethod | None
top_defense: TopMethod | None
recent_submissions: list[RecentSubmission]


@router.get("", response_model=DashboardResponse)
async def get_dashboard(session: AsyncSession = Depends(get_session)):
total_submissions = (await session.execute(select(func.count(Submission.id)))).scalar() or 0

completed_evaluations = (
await session.execute(select(func.count(EvaluationJob.id)).where(EvaluationJob.status == "completed"))
).scalar() or 0

failed_evaluations = (
await session.execute(select(func.count(EvaluationJob.id)).where(EvaluationJob.status == "failed"))
).scalar() or 0

active_eval = (
await session.execute(
select(func.count(EvaluationJob.id)).where(EvaluationJob.status.in_(["running", "queued"]))
)
).scalar() or 0

active_bench = (
await session.execute(select(func.count(BenchJob.id)).where(BenchJob.status.in_(["running", "queued"])))
).scalar() or 0

queue_pending = get_queue().pending_count()

top_attack = await _top_method(session, "attack")
top_defense = await _top_method(session, "defense")

recent_stmt = select(Submission).order_by(Submission.created_at.desc(), Submission.id.desc()).limit(8)
recent_rows = (await session.execute(recent_stmt)).scalars().all()
recent_submissions = [
RecentSubmission(
id=s.id,
display_name=s.display_name,
method_name=s.method_name,
role=s.role,
status=s.status,
created_at=str(s.created_at),
)
for s in recent_rows
]

return DashboardResponse(
total_submissions=total_submissions,
completed_evaluations=completed_evaluations,
failed_evaluations=failed_evaluations,
active_jobs=active_eval + active_bench,
queue_pending=queue_pending,
top_attack=top_attack,
top_defense=top_defense,
recent_submissions=recent_submissions,
)


async def _top_method(session: AsyncSession, role: str) -> TopMethod | None:
import json

stmt = (
select(Submission, EvaluationJob)
.join(EvaluationJob, EvaluationJob.submission_id == Submission.id)
.where(Submission.role == role, Submission.status == "completed", EvaluationJob.status == "completed")
.order_by(EvaluationJob.completed_at.desc())
)
rows = (await session.execute(stmt)).all()

best: TopMethod | None = None
best_score: float | None = None

for sub, job in rows:
if not job.results_json:
continue
results = json.loads(job.results_json)
accs = [r["avg_final_accuracy"] for r in results.values() if r.get("avg_final_accuracy") is not None]
if not accs:
continue
avg = sum(accs) / len(accs)
is_better = (
best_score is None or (role == "attack" and avg < best_score) or (role == "defense" and avg > best_score)
)
if is_better:
best_score = avg
best = TopMethod(
submission_id=sub.id,
display_name=sub.display_name,
method_name=sub.method_name,
author=sub.author,
avg_accuracy=avg,
)

return best
63 changes: 62 additions & 1 deletion apps/backend/app/api/v1/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select

from ...db import get_session
from ...models import EvaluationJob
from ...models import BenchJob, EvaluationJob, Submission
from ...schemas import JobProgress
from ...services.task_queue import get_queue

Expand All @@ -21,6 +22,66 @@ class QueueStatusResponse(BaseModel):
tasks: list[dict]


class JobListItem(BaseModel):
id: int
job_type: str # "evaluation" | "bench"
status: str
label: str
submission_id: int | None = None
progress: str | None = None
error: str | None = None
started_at: str | None = None
completed_at: str | None = None
created_at: str


@router.get("", response_model=list[JobListItem])
async def list_jobs(session: AsyncSession = Depends(get_session)):
eval_stmt = (
select(EvaluationJob, Submission.display_name)
.join(Submission, Submission.id == EvaluationJob.submission_id, isouter=True)
.order_by(EvaluationJob.created_at.desc())
.limit(50)
)
eval_rows = (await session.execute(eval_stmt)).all()

bench_stmt = select(BenchJob).order_by(BenchJob.bench_created_at.desc()).limit(50)
bench_rows = (await session.execute(bench_stmt)).scalars().all()

items: list[JobListItem] = []
for job, display_name in eval_rows:
items.append(
JobListItem(
id=job.id,
job_type="evaluation",
status=job.status,
label=display_name or f"Submission #{job.submission_id}",
submission_id=job.submission_id,
progress=job.progress,
error=job.error,
started_at=str(job.started_at) if job.started_at else None,
completed_at=str(job.completed_at) if job.completed_at else None,
created_at=str(job.created_at),
)
)
for job in bench_rows:
items.append(
JobListItem(
id=job.id,
job_type="bench",
status=job.status,
label=job.prompt[:80] if job.prompt else f"Bench #{job.id}",
error=job.error,
started_at=str(job.started_at) if job.started_at else None,
completed_at=str(job.completed_at) if job.completed_at else None,
created_at=str(job.bench_created_at),
)
)

items.sort(key=lambda x: x.created_at, reverse=True)
return items[:50]


@router.get("/queue/status", response_model=QueueStatusResponse)
async def queue_status():
"""Get current task queue status."""
Expand Down
Loading
Loading