Compare commits

...

2 Commits

Author SHA1 Message Date
e8dcb5fcaf feat(ui): show private activity count on timeline when no public events
When viewing a date range with zero public activities, the status line
now shows the count of private contributions (derived from daily counts
which include private repos). Helps explain gaps in the public timeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 15:41:22 +03:00
b41e8c330a feat: include private repo contributions in graph metrics
Aggregate graph endpoints (daily counts, language daily counts, source
summaries, OG image) now include private repository activity. These
endpoints only expose numeric counts — no commit messages, repo names,
or other metadata — so private details remain hidden. The activity
timeline continues to serve only public events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 15:35:22 +03:00
4 changed files with 28 additions and 13 deletions

View File

@@ -130,7 +130,7 @@ async fn list_sources(
) -> Result<Json<Vec<SourceSummary>>, ApiError> {
let summaries = state
.store
.source_summaries(/* include_private */ false)
.source_summaries(/* include_private */ true)
.await
.map_err(internal)?;
Ok(Json(summaries))
@@ -155,7 +155,7 @@ async fn daily_counts(
) -> Result<Json<Vec<DailyCount>>, ApiError> {
let to = params.to.unwrap_or_else(|| Utc::now().date_naive());
let from = params.from.unwrap_or_else(|| to - chrono::Duration::days(365));
let counts = state.store.daily_counts(from, to).await.map_err(internal)?;
let counts = state.store.daily_counts(from, to, /* include_private */ true).await.map_err(internal)?;
Ok(Json(counts))
}
@@ -165,7 +165,7 @@ async fn language_daily_counts(
) -> Result<Json<Vec<LanguageDailyCount>>, ApiError> {
let to = params.to.unwrap_or_else(|| Utc::now().date_naive());
let from = params.from.unwrap_or_else(|| to - chrono::Duration::days(365));
let counts = state.store.language_daily_counts(from, to).await.map_err(internal)?;
let counts = state.store.language_daily_counts(from, to, /* include_private */ true).await.map_err(internal)?;
Ok(Json(counts))
}
@@ -182,7 +182,7 @@ async fn og_contributions(
// Get date range from source summaries
let summaries = state
.store
.source_summaries(false)
.source_summaries(/* include_private */ true)
.await
.map_err(internal)?;
let earliest = summaries
@@ -195,7 +195,7 @@ async fn og_contributions(
let counts = state
.store
.daily_counts(earliest, today)
.daily_counts(earliest, today, /* include_private */ true)
.await
.map_err(internal)?;

View File

@@ -20,8 +20,8 @@ pub trait EventReader: Send + Sync {
async fn list_events(&self, query: &EventQuery) -> Result<Vec<Event>, StoreError>;
async fn source_summaries(&self, include_private: bool) -> Result<Vec<SourceSummary>, StoreError>;
async fn list_projects(&self) -> Result<Vec<ProjectSummary>, StoreError>;
async fn daily_counts(&self, from: NaiveDate, to: NaiveDate) -> Result<Vec<DailyCount>, StoreError>;
async fn language_daily_counts(&self, from: NaiveDate, to: NaiveDate) -> Result<Vec<LanguageDailyCount>, StoreError>;
async fn daily_counts(&self, from: NaiveDate, to: NaiveDate, include_private: bool) -> Result<Vec<DailyCount>, StoreError>;
async fn language_daily_counts(&self, from: NaiveDate, to: NaiveDate, include_private: bool) -> Result<Vec<LanguageDailyCount>, StoreError>;
async fn repo_languages(&self) -> Result<Vec<RepoLanguage>, StoreError>;
}

View File

@@ -196,7 +196,7 @@ impl EventReader for PgStore {
.collect()
}
async fn daily_counts(&self, from: NaiveDate, to: NaiveDate) -> Result<Vec<DailyCount>, StoreError> {
async fn daily_counts(&self, from: NaiveDate, to: NaiveDate, include_private: bool) -> Result<Vec<DailyCount>, StoreError> {
let rows = sqlx::query(
r#"
SELECT d::date AS date,
@@ -205,13 +205,14 @@ impl EventReader for PgStore {
LEFT JOIN events e
ON e.occurred_at >= (d::date || 'T00:00:00Z')::timestamptz
AND e.occurred_at < ((d::date + 1) || 'T00:00:00Z')::timestamptz
AND e.public = true
AND ($3::bool OR e.public = true)
GROUP BY d::date
ORDER BY d::date
"#,
)
.bind(from)
.bind(to)
.bind(include_private)
.fetch_all(&self.pool)
.await
.map_err(map_err)?;
@@ -226,7 +227,7 @@ impl EventReader for PgStore {
.collect()
}
async fn language_daily_counts(&self, from: NaiveDate, to: NaiveDate) -> Result<Vec<LanguageDailyCount>, StoreError> {
async fn language_daily_counts(&self, from: NaiveDate, to: NaiveDate, include_private: bool) -> Result<Vec<LanguageDailyCount>, StoreError> {
let rows = sqlx::query(
r#"
SELECT date, language, color,
@@ -244,7 +245,7 @@ impl EventReader for PgStore {
JOIN events e
ON e.occurred_at >= (d::date || 'T00:00:00Z')::timestamptz
AND e.occurred_at < ((d::date + 1) || 'T00:00:00Z')::timestamptz
AND e.public = true
AND ($3::bool OR e.public = true)
AND e.action IN ('Commit', 'PushEvent', 'commit_repo')
JOIN repo_languages rl
ON rl.source = e.source
@@ -273,6 +274,7 @@ impl EventReader for PgStore {
)
.bind(from)
.bind(to)
.bind(include_private)
.fetch_all(&self.pool)
.await
.map_err(map_err)?;

View File

@@ -5,7 +5,7 @@ import Col from 'react-bootstrap/Col';
import Row from 'react-bootstrap/Row';
import { VerticalTimeline } from 'react-vertical-timeline-component';
import { fetchEvents, fetchSources, type Source } from '../api/client';
import { fetchDailyCounts, fetchEvents, fetchSources, type Source } from '../api/client';
import { Filters } from '../components/Filters';
import { TimelineEntry } from '../components/TimelineEntry';
@@ -82,6 +82,19 @@ export function TimelineHome() {
const events = eventsQ.data ?? [];
const fromStr = new Date(rangeValue[0]).toISOString().slice(0, 10);
const toStr = new Date(rangeValue[1]).toISOString().slice(0, 10);
const dailyQ = useQuery({
queryKey: ['daily-counts', fromStr, toStr],
queryFn: () => fetchDailyCounts(fromStr, toStr),
staleTime: 5 * 60_000,
});
const totalCount = useMemo(
() => (dailyQ.data ?? []).reduce((sum, d) => sum + d.count, 0),
[dailyQ.data],
);
const privateCount = totalCount - events.length;
return (
<>
<Filters
@@ -105,7 +118,7 @@ export function TimelineHome() {
? 'loading…'
: eventsQ.isError
? `error: ${(eventsQ.error as Error).message}`
: `showing ${events.length} ${events.length === 1 ? 'activity' : 'activities'}`}
: `showing ${events.length} public ${events.length === 1 ? 'activity' : 'activities'}${privateCount > 0 ? `, ${privateCount} private` : ''}`}
</p>
<VerticalTimeline>
{events.map((item) => (