From 9df381541788aae981c9a2f3bc82075c78776193 Mon Sep 17 00:00:00 2001 From: timmy Date: Wed, 5 Aug 2026 04:04:40 +0000 Subject: [PATCH] feat: add realtime Gitea event stream widget --- frontend/index.html | 28 +++++++++++++++++ src/gitea_proxy.py | 14 +++++++++ src/main.py | 7 ++++- tests/test_event_stream.py | 64 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 tests/test_event_stream.py diff --git a/frontend/index.html b/frontend/index.html index cae60a2..f5845e3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -52,6 +52,8 @@ textarea { resize: vertical; min-height: 120px; } .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 8px #22c55e; } .widget { border: 1px solid #1b2d45; border-radius: 12px; padding: 10px; background: linear-gradient(180deg,#0f1d33,#0b1526); } .widget h3 { margin: 4px 0 8px; font-size: 13px; color: #7aa1c9; } +.event { padding: 8px 0; border-bottom: 1px solid #1b2d45; } +.event:last-child { border-bottom: 0; } .obi { width:14px; height:14px; background: url('data:image/svg+xml;utf8,') center/contain no-repeat; display:inline-block; } .footer { padding: 12px; text-align: center; color:#4e6b8a; font-size:12px; } @keyframes fadein { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } @@ -97,6 +99,10 @@ textarea { resize: vertical; min-height: 120px; }

Pull Requests

Loading…
+
+

Gitea event stream

+
Loading…
+

Live Widgets

@@ -229,6 +235,26 @@ textarea { resize: vertical; min-height: 120px; } el.innerHTML = deltas.length ? deltas.map(d => '
' + escapeHtml(d.priority) + ' ' + escapeHtml(d.action) + ' ' + escapeHtml(d.target || '') + '
' + escapeHtml(d.panel) + '
').join('') : '
No suggestions yet.
'; } + function paintEventStream(events) { + const el = qs('#gitea-events'); + el.innerHTML = events.length ? events.slice(0, 12).map(event => + '
' + escapeHtml(event.actor?.login || 'Gitea') + ' ' + + escapeHtml(event.type || 'activity') + + '
' + escapeHtml(event.repo?.full_name || '') + + (event.created_at ? ' · ' + escapeHtml(fmt(event.created_at)) : '') + '
' + ).join('') : '
No recent Gitea activity.
'; + } + + async function loadEventStream() { + try { + const res = await fetch('/api/v1/events', { headers: { Accept: 'application/json' } }); + if (!res.ok) throw new Error('HTTP ' + res.status); + paintEventStream(await res.json()); + } catch (e) { + qs('#gitea-events').innerHTML = '
Event stream unavailable.
'; + } + } + async function tickWidgets() { try { const res = await fetch('/api/v1/context', { headers: { Accept: 'application/json' } }); @@ -318,7 +344,9 @@ textarea { resize: vertical; min-height: 120px; } qs('#refresh').addEventListener('click', load); load(); + loadEventStream(); setInterval(load, 8000); + setInterval(loadEventStream, 5000); setInterval(tickWidgets, 2000); tickWidgets(); diff --git a/src/gitea_proxy.py b/src/gitea_proxy.py index c45bdc7..6d04c8e 100644 --- a/src/gitea_proxy.py +++ b/src/gitea_proxy.py @@ -35,3 +35,17 @@ async def issues() -> list[dict]: async def pull_requests() -> list[dict]: return await fetch("user/pulls?limit=50") + + +async def activity_events() -> list[dict]: + user = await current_user() + events = await fetch(f"users/{user['login']}/activities/feeds?limit=20") + return [ + { + "type": event.get("op_type", "activity"), + "actor": event.get("act_user") or {}, + "repo": event.get("repo") or {}, + "created_at": event.get("created", ""), + } + for event in events + ] diff --git a/src/main.py b/src/main.py index 6da7ab4..c07ffb2 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles -from src.gitea_proxy import current_user, repos, issues, pull_requests +from src.gitea_proxy import activity_events, current_user, repos, issues, pull_requests from src.models import User, Repo, Issue, PullRequest from src.suggestion_engine import compute from src.views import router as frontend_router @@ -72,3 +72,8 @@ async def context() -> JSONResponse: "view": ctx.view, "deltas": [d.dict() for d in ctx.deltas], }) + + +@app.get("/api/v1/events") +async def event_stream() -> list[dict]: + return await activity_events() diff --git a/tests/test_event_stream.py b/tests/test_event_stream.py new file mode 100644 index 0000000..a76ae13 --- /dev/null +++ b/tests/test_event_stream.py @@ -0,0 +1,64 @@ +from pathlib import Path + +import pytest + +from src import main +from src import gitea_proxy + + +DASHBOARD = Path("frontend/index.html") + + +def test_dashboard_has_realtime_gitea_event_stream_widget(): + html = DASHBOARD.read_text() + + assert "Gitea event stream" in html + assert "id=\"gitea-events\"" in html + assert "function paintEventStream" in html + assert "setInterval(loadEventStream, 5000)" in html + assert "event.actor?.login" in html + assert "event.repo?.full_name" in html + + +@pytest.mark.anyio +async def test_event_stream_returns_recent_authenticated_gitea_activity(monkeypatch): + expected = [{"type": "create", "actor": {"login": "timmy"}}] + + async def fake_activity_events(): + return expected + + monkeypatch.setattr(main, "activity_events", fake_activity_events) + + response = await main.event_stream() + + assert response == expected + + +@pytest.mark.anyio +async def test_activity_events_fetches_feed_for_authenticated_user(monkeypatch): + paths = [] + + async def fake_current_user(): + return {"login": "timmy"} + + async def fake_fetch(path): + paths.append(path) + return [{ + "op_type": "push", + "act_user": {"login": "timmy"}, + "repo": {"full_name": "stackchain/stackchain-dashboard"}, + "created": "2026-08-05T03:00:00Z", + }] + + monkeypatch.setattr(gitea_proxy, "current_user", fake_current_user) + monkeypatch.setattr(gitea_proxy, "fetch", fake_fetch) + + result = await gitea_proxy.activity_events() + + assert result == [{ + "type": "push", + "actor": {"login": "timmy"}, + "repo": {"full_name": "stackchain/stackchain-dashboard"}, + "created_at": "2026-08-05T03:00:00Z", + }] + assert paths == ["users/timmy/activities/feeds?limit=20"] -- 2.43.0