feat(stats): split repos public/private + 7-day daily commit series

Collector now reports repos_public/repos_private (repos kept as the sum)
and a commits_daily[] series of 7 {date,commits} buckets (UTC days,
zero-filled), bucketed in Go from raw created_unix to avoid MySQL
session-timezone ambiguity. commits_7d is now the sum of those buckets
so the card total matches the sparkline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-29 08:09:21 +01:00
parent 2cfa6ca76a
commit 448254001a
2 changed files with 94 additions and 19 deletions

View File

@@ -20,14 +20,23 @@ import (
_ "github.com/go-sql-driver/mysql"
)
// DailyCommit is one bucket of the last-7-days commit series (UTC days).
type DailyCommit struct {
Date string `json:"date"` // YYYY-MM-DD (UTC)
Commits int64 `json:"commits"`
}
type Stats struct {
Repos int64 `json:"repos"`
Agents int64 `json:"agents"`
Commits7d int64 `json:"commits_7d"`
PRs int64 `json:"prs"`
Merged int64 `json:"merged"`
GeneratedAt string `json:"generated_at"`
IntervalH int `json:"interval_hours"`
Repos int64 `json:"repos"` // public + private
ReposPublic int64 `json:"repos_public"`
ReposPrivate int64 `json:"repos_private"`
Agents int64 `json:"agents"`
Commits7d int64 `json:"commits_7d"` // sum of CommitsDaily
CommitsDaily []DailyCommit `json:"commits_daily"`
PRs int64 `json:"prs"`
Merged int64 `json:"merged"`
GeneratedAt string `json:"generated_at"`
IntervalH int `json:"interval_hours"`
}
var (
@@ -56,18 +65,59 @@ func collect(dsn string, interval time.Duration) (*Stats, error) {
var s Stats
err = db.QueryRowContext(ctx, `
SELECT
(SELECT COUNT(*) FROM repository),
(SELECT COUNT(*) FROM repository WHERE is_private=0),
(SELECT COUNT(*) FROM repository WHERE is_private=1),
(SELECT COUNT(*) FROM user WHERE type=0 AND is_admin=0),
(SELECT COALESCE(SUM(CASE WHEN JSON_VALID(content)
THEN COALESCE(JSON_LENGTH(content,'$.Commits'),0) ELSE 0 END),0)
FROM action
WHERE op_type=5 AND created_unix > UNIX_TIMESTAMP()-604800),
(SELECT COUNT(*) FROM pull_request),
(SELECT COUNT(*) FROM pull_request WHERE has_merged=1)
`).Scan(&s.Repos, &s.Agents, &s.Commits7d, &s.PRs, &s.Merged)
`).Scan(&s.ReposPublic, &s.ReposPrivate, &s.Agents, &s.PRs, &s.Merged)
if err != nil {
return nil, err
}
s.Repos = s.ReposPublic + s.ReposPrivate
// Per-day commit counts for the last 7 calendar days (UTC), oldest first.
// We pull raw (created_unix, commit_count) rows and bucket them in Go to
// avoid any dependence on the MySQL session time zone in date functions.
now := time.Now().UTC()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
start := today.AddDate(0, 0, -6) // 7 buckets: start .. today
startUnix := start.Unix()
rows, err := db.QueryContext(ctx, `
SELECT created_unix,
CASE WHEN JSON_VALID(content)
THEN COALESCE(JSON_LENGTH(content,'$.Commits'),0) ELSE 0 END
FROM action
WHERE op_type=5 AND created_unix >= ?`, startUnix)
if err != nil {
return nil, err
}
defer rows.Close()
daily := make([]int64, 7)
for rows.Next() {
var cu, c int64
if err := rows.Scan(&cu, &c); err != nil {
return nil, err
}
if idx := int((cu - startUnix) / 86400); idx >= 0 && idx < 7 {
daily[idx] += c
}
}
if err := rows.Err(); err != nil {
return nil, err
}
s.CommitsDaily = make([]DailyCommit, 7)
for i := 0; i < 7; i++ {
s.CommitsDaily[i] = DailyCommit{
Date: start.AddDate(0, 0, i).Format("2006-01-02"),
Commits: daily[i],
}
s.Commits7d += daily[i]
}
s.GeneratedAt = time.Now().UTC().Format(time.RFC3339)
s.IntervalH = int(interval / time.Hour)
return &s, nil
@@ -99,8 +149,8 @@ func refresh(dsn, out string, interval time.Duration) {
mu.Lock()
last = s
mu.Unlock()
log.Printf("ok repos=%d agents=%d commits_7d=%d prs=%d merged=%d",
s.Repos, s.Agents, s.Commits7d, s.PRs, s.Merged)
log.Printf("ok repos=%d (pub=%d priv=%d) agents=%d commits_7d=%d prs=%d merged=%d",
s.Repos, s.ReposPublic, s.ReposPrivate, s.Agents, s.Commits7d, s.PRs, s.Merged)
}
func main() {