feat(ui): project drill-down route with repo-filtered event timeline

Add repo filter param to /v1/events (SQL COALESCE across payload
shapes per source). New /project/:source/* route renders a filtered
activity timeline for a single repo. Dashboard cards link to the
drill-down page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 15:22:11 +03:00
parent a70fab4feb
commit 80f3f7c5cb
7 changed files with 73 additions and 24 deletions

View File

@@ -82,6 +82,8 @@ struct EventsQueryParams {
to: Option<DateTime<Utc>>,
/// Comma-separated list, e.g. `source=github,gitea`.
source: Option<String>,
/// Filter to a specific repo, e.g. `repo=grenade/moments`.
repo: Option<String>,
limit: Option<u32>,
}
@@ -101,6 +103,7 @@ async fn list_events(
from: params.from,
to: params.to,
sources,
repo: params.repo,
// Public timeline only — private events stay in the DB but are never
// surfaced. A future authenticated path can flip this.
include_private: false,

View File

@@ -54,6 +54,12 @@ impl EventReader for PgStore {
AND ($2::timestamptz IS NULL OR occurred_at < $2)
AND ($3::text[] IS NULL OR source = ANY($3))
AND ($4::bool OR public = true)
AND ($6::text IS NULL OR COALESCE(
payload->'repo'->>'name',
payload->'repository'->>'full_name',
payload->>'_repo',
payload->>'product'
) = $6)
ORDER BY occurred_at DESC
LIMIT $5
"#,
@@ -63,6 +69,7 @@ impl EventReader for PgStore {
.bind(sources.as_deref())
.bind(query.include_private)
.bind(query.limit as i64)
.bind(query.repo.as_deref())
.fetch_all(&self.pool)
.await
.map_err(map_err)?;

View File

@@ -67,6 +67,8 @@ pub struct EventQuery {
pub from: Option<DateTime<Utc>>,
pub to: Option<DateTime<Utc>>,
pub sources: Option<Vec<Source>>,
/// Filter to events matching a specific repo (matched against payload).
pub repo: Option<String>,
/// When false (default), only `public = true` rows are returned. The API
/// pins this to false today; a future authenticated path can flip it.
pub include_private: bool,