mirror of
https://github.com/tiennm99/ghstats.git
synced 2026-08-05 12:23:04 +00:00
docs: plan for records card (260509-0913)
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
---
|
||||
phase: 1
|
||||
title: "Records computation helpers"
|
||||
status: completed
|
||||
priority: P1
|
||||
effort: "1h"
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: Records computation helpers
|
||||
|
||||
## Overview
|
||||
|
||||
Add pure functions that derive the 6 records from `Profile` data already in memory. Live in `internal/card/records.go` next to the card itself — no new public package surface needed.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- `peakDay(days []DailyContribution) (count int, date time.Time)` — argmax; ties → earliest date
|
||||
- `peakMonth(days []DailyContribution) (count int, ym time.Time)` — sum per `YYYY-MM`, argmax; ties → earliest month
|
||||
- `firstActiveDay(days []DailyContribution) time.Time` — first day with `Count > 0`; zero time if none
|
||||
- `activeDaysCount(days []DailyContribution) int` — count of days where `Count > 0`
|
||||
- `accountAgeYears(createdAt, now time.Time) float64` — fractional years, 1 decimal
|
||||
- `languagesUsed(stats []LangStat) int` — `len(stats)` (already deduped upstream)
|
||||
|
||||
**Non-functional**
|
||||
- O(n) over `DailyContributionsAllTime` for all daily-derived records (single pass acceptable)
|
||||
- Zero allocations for argmax helpers (just iterate)
|
||||
- All helpers package-private (`unexported`) — only the card uses them
|
||||
|
||||
## Architecture
|
||||
|
||||
Single file `internal/card/records.go` exporting the `recordsCard` struct (Phase 2) and these helpers. No mutation of `Profile`. Empty-input handling: return zero values; the card layer decides what to display.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Create: `internal/card/records.go` (helpers section)
|
||||
- Read for context: `internal/github/model.go` (Profile fields), `internal/card/stats.go` (card pattern)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create `internal/card/records.go` with package-private helpers.
|
||||
2. Implement `peakDay`: iterate, track `(maxCount, earliestDate)`; tie-break on earlier date.
|
||||
3. Implement `peakMonth`: bucket sum into a `map[time.Time]int` keyed by `time.Date(year,month,1,...)`; argmax with same tie-break.
|
||||
4. Implement `firstActiveDay`: first index where `Count > 0`; assumes input is chronological (matches existing fetcher).
|
||||
5. Implement `activeDaysCount`: counter of `Count > 0`.
|
||||
6. Implement `accountAgeYears`: `now.Sub(createdAt).Hours() / 24 / 365.25`, round to 1 decimal.
|
||||
7. Implement `languagesUsed`: trivial wrapper for symmetry/testability.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All 6 helpers compile (`go build ./...`)
|
||||
- [ ] Helpers produce correct results on hand-rolled fixtures
|
||||
- [ ] Empty-data inputs return zero values without panic
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** `peakMonth` map ordering non-deterministic — Go map iteration is randomized, so two months tied at the same count may produce different "winners" across runs. **Mitigation:** scan keys, sort by date ascending, then argmax on first occurrence. Alternative: track `(count, ym)` during the bucket-fill pass instead of a second pass.
|
||||
- **Risk:** `accountAgeYears` rounding inconsistent with existing profile-card "age" string. **Mitigation:** match `internal/card/profile.go` convention (check there before implementing).
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
phase: 2
|
||||
title: "SVG card rendering"
|
||||
status: completed
|
||||
priority: P1
|
||||
effort: "1.5h"
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 2: SVG card rendering
|
||||
|
||||
## Overview
|
||||
|
||||
Render the 6 records as a `records.svg` card mirroring `stats.svg`'s row layout (icon + label + right-aligned accent value). Register the card in `allCards` so `RenderAll` picks it up.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- File output: `records.svg` (340×200, matches all other cards)
|
||||
- Title: `Records (all time)`
|
||||
- 6 rows in this order:
|
||||
1. ⚡ **Best day** — `{count} on YYYY-MM-DD`
|
||||
2. 🔥 **Best month** — `{count} in MMM YYYY` (e.g. `612 in Mar 2026`)
|
||||
3. 🌱 **First contribution** — `YYYY-MM-DD`
|
||||
4. 📆 **Active days** — `{formatInt(count)}`
|
||||
5. ⏳ **On GitHub** — `{years.1f} years`
|
||||
6. 🌐 **Languages used** — `{count}`
|
||||
- Empty-data fallback: render the card with rows showing `—` (em-dash) for missing values, **never panic**
|
||||
|
||||
**Non-functional**
|
||||
- Reuse `header()` / `footer` helpers from `internal/card/svg.go`
|
||||
- Reuse `escapeXML()` and `formatInt()` from existing helpers
|
||||
- Octicon paths live in `icons.go` (1-3 new icons may be needed)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
internal/card/records.go
|
||||
├── recordsCard struct{}
|
||||
├── (recordsCard) Filename() string → "records.svg"
|
||||
├── (recordsCard) SVG(p, t) → builds 6 statRow-style entries, emits SVG
|
||||
└── helpers from Phase 1
|
||||
```
|
||||
|
||||
Register in `internal/card/card.go`:
|
||||
```go
|
||||
var allCards = []Card{
|
||||
...
|
||||
contributionsByYearCard{},
|
||||
recordsCard{}, // new — appended last
|
||||
}
|
||||
```
|
||||
|
||||
**Icon strategy (minimize new octicon paths):**
|
||||
- Best day → `iconCommit` (existing, fits "activity peak")
|
||||
- Best month → `iconStar` (existing, fits "highlight")
|
||||
- First contribution → **new** `iconCalendar` octicon
|
||||
- Active days → `iconRepos` (existing, decent fallback) OR **new** `iconHistory`
|
||||
- On GitHub → `iconClock` (existing)
|
||||
- Languages used → **new** `iconGlobe` octicon
|
||||
|
||||
Net: **2-3 new octicon paths** added to `icons.go` from primer/octicons. Final icon set decided during implementation — fewer-new is fine if reuse looks clean.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Create: `internal/card/records.go`
|
||||
- Modify: `internal/card/card.go` (register `recordsCard{}` in `allCards`)
|
||||
- Modify: `internal/card/icons.go` (add 2-3 octicon paths)
|
||||
- Read for context: `internal/card/stats.go`, `internal/card/svg.go`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Add octicon paths to `icons.go` (copy from `primer/octicons` 16×16 viewBox).
|
||||
2. Create `recordsCard` struct + `Filename()`.
|
||||
3. Implement `SVG()`:
|
||||
- Compute 6 records via Phase-1 helpers (`time.Now()` for `accountAgeYears`).
|
||||
- Build `[]statRow` (or local equivalent) with formatted strings.
|
||||
- Empty-data branch: if `len(p.DailyContributionsAllTime) == 0`, label/value rows still render but values are `—`.
|
||||
- Emit header → 6 rows (same coords as stats card: `rowX=20, rowY0=55, rowDY=20, iconSize=12, valueX=320`) → footer.
|
||||
4. Register in `allCards`.
|
||||
5. `go build ./...` — confirm clean compile.
|
||||
6. Run end-to-end render against the demo fixture if available, else inspect generated SVG manually.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `go build ./...` clean
|
||||
- [ ] Card filename `records.svg` registered and produced by `RenderAll`
|
||||
- [ ] SVG validates (no XML parse errors) for all 65 themes
|
||||
- [ ] Empty-data fixture produces a legible card (no panic, no missing values causing layout shift)
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** Row spacing collides at 6 rows × 20px DY + 55px Y0 = 175px; fits within 200px frame. **Mitigation:** verified against stats card (which has 7 rows at the same cadence). Safe.
|
||||
- **Risk:** Long values (e.g. "612 in Mar 2026") overrun the value column at narrow themes. **Mitigation:** value text uses `text-anchor="end"` at `valueX=320` — overflows go left, never clip. If labels collide with values on a row, shorten label phrasing (e.g. "Best month" not "Best month ever").
|
||||
- **Risk:** New octicon paths copied incorrectly (broken SVG). **Mitigation:** copy from `primer/octicons` source; smoke-test render in browser.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
phase: 3
|
||||
title: "Tests + README"
|
||||
status: completed
|
||||
priority: P2
|
||||
effort: "45m"
|
||||
dependencies: [1, 2]
|
||||
---
|
||||
|
||||
# Phase 3: Tests + README
|
||||
|
||||
## Overview
|
||||
|
||||
Cover the 6 record helpers with unit tests, add a card-level smoke test, and document the new card in `README.md` so it shows up in the gallery preview.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- Helpers tested for: empty input, single-day, ties, all-zero counts
|
||||
- Card-level test asserts `records.svg` filename + key strings appear (label rows, formatted values)
|
||||
- README updated:
|
||||
- New row in card table at the same row index where it lands in `allCards`
|
||||
- Dracula preview cell pointing at `./demo/dracula/records.svg`
|
||||
|
||||
**Non-functional**
|
||||
- Test file follows existing convention (`*_test.go` in `internal/card`)
|
||||
- No flaky time-based tests — pass `now` explicitly into `accountAgeYears`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
internal/card/records_test.go
|
||||
├── TestPeakDay (incl. ties → earliest)
|
||||
├── TestPeakMonth (incl. ties → earliest, multi-month)
|
||||
├── TestFirstActiveDay (incl. all-zero → zero time)
|
||||
├── TestActiveDaysCount
|
||||
├── TestAccountAgeYears (fixed createdAt vs fixed now)
|
||||
├── TestLanguagesUsed
|
||||
└── TestRecordsCardSVG (smoke: contains title + 6 row labels)
|
||||
```
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Create: `internal/card/records_test.go`
|
||||
- Modify: `README.md` (card table + dracula gallery cell)
|
||||
- Read for context: `internal/card/weekday_start_test.go` (existing test patterns)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Write helper unit tests with hand-rolled `[]DailyContribution` fixtures.
|
||||
2. Write card-level test asserting:
|
||||
- SVG bytes contain `Records (all time)` title
|
||||
- SVG contains 6 row labels (`Best day`, `Best month`, ...)
|
||||
- Empty-fixture variant still renders without panic
|
||||
3. `go test ./...` — confirm pass.
|
||||
4. Update README:
|
||||
- Card table: add row `15` (or whatever index) with description
|
||||
- Gallery: add `<td><img src="./demo/dracula/records.svg" alt="records" /></td>` to dracula preview block
|
||||
5. Trigger demo workflow regen (or rely on next push).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All new tests pass (`go test ./...`)
|
||||
- [ ] README renders correctly (card table + gallery image link)
|
||||
- [ ] Demo workflow produces `records.svg` for all 65 themes after merge
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk:** Card-level snapshot test gets brittle as themes evolve. **Mitigation:** assert on text fragments only, never on full byte equality.
|
||||
- **Risk:** README gallery layout breaks if the new image cell forces an odd column count. **Mitigation:** keep dracula gallery's `<table>` row count even (pair with another card or span 2 cols).
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Add records card
|
||||
status: completed
|
||||
created: 2026-05-09
|
||||
slug: records-card
|
||||
---
|
||||
|
||||
# Add records card
|
||||
|
||||
A new SVG card surfacing **personal extremes & milestones** rather than aggregates. Mirrors `stats.svg` layout (icon + label + right-aligned accent value).
|
||||
|
||||
## Goal
|
||||
|
||||
Show 6 records that tell a story arc — **origin → tenure → peaks → breadth**:
|
||||
|
||||
| Row | Record | Source |
|
||||
|---|---|---|
|
||||
| 1 | Best day (count + date) | argmax over `DailyContributionsAllTime` |
|
||||
| 2 | Best month (count + `YYYY-MM`) | bucket-sum over `DailyContributionsAllTime`, argmax |
|
||||
| 3 | First contribution (date) | first non-zero day in `DailyContributionsAllTime` |
|
||||
| 4 | Active days (lifetime) | count non-zero days in `DailyContributionsAllTime` |
|
||||
| 5 | On GitHub (years) | `now − Profile.CreatedAt` |
|
||||
| 6 | Languages used | `len(Profile.CommitsByLanguageAllTime)` |
|
||||
|
||||
**No new GraphQL queries.** All data already populated by existing fetchers.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Card #2 variant for last-year records (low-value duplication)
|
||||
- Streak/top-repo/peak-year (already covered by other cards)
|
||||
- Configurable record set (YAGNI)
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Status | File |
|
||||
|---|---|---|---|
|
||||
| 1 | Records computation helpers | completed | [phase-01-records-computation.md](./phase-01-records-computation.md) |
|
||||
| 2 | SVG card rendering | completed | [phase-02-svg-card-rendering.md](./phase-02-svg-card-rendering.md) |
|
||||
| 3 | Tests + README | completed | [phase-03-tests-and-readme.md](./phase-03-tests-and-readme.md) |
|
||||
|
||||
## Key dependencies
|
||||
|
||||
- Existing fetched data: `Profile.DailyContributionsAllTime`, `Profile.CommitsByLanguageAllTime`, `Profile.CreatedAt`
|
||||
- Existing card pattern: `internal/card/stats.go` (icon + label + value rows)
|
||||
- Existing icon set: `internal/card/icons.go` (may need 1-3 new octicon paths)
|
||||
|
||||
## Definition of done
|
||||
|
||||
- `records.svg` renders for all 65 themes via demo workflow
|
||||
- Card registered in `allCards` (renders alongside other cards)
|
||||
- Unit tests cover record-extraction edge cases (empty data, single-day, ties)
|
||||
- README updated with new row in card table + dracula preview
|
||||
Reference in New Issue
Block a user