Files
tiennm99 ad775adffa feat(web): add web UI and Coolify compose deployment
`ghglance -serve :8080` runs a web UI from the same binary: submit a
username, a background job renders every card in every theme, and
/u/<user> shows them again with a theme picker and embed URLs.

- Cards are stored on disk with atomic symlink publishing and deleted
  after -retention (default 24h); -cooldown limits token-less regeneration.
- Token-less jobs use a public-only server token with private and org
  scope forced off; a submitter's own token is used for that job only and
  never stored or logged. The form links to GitHub's new-token page with
  the needed scopes pre-ticked.
- The CLI and web share one fetch helper; CLI output is unchanged.
- compose.yml deploys to Coolify with a data volume and /healthz check.
2026-10-07 11:08:07 +07:00

69 lines
1.8 KiB
Go

// Package card renders Profile data into SVG cards on disk.
package card
import (
"fmt"
"os"
"path/filepath"
"github.com/tiennm99/ghglance/internal/github"
"github.com/tiennm99/ghglance/internal/theme"
)
// Card renders one SVG for a Profile under the given theme.
type Card interface {
// Filename is the on-disk basename (e.g. "profile-details.svg").
Filename() string
// SVG returns the rendered SVG bytes.
SVG(p *github.Profile, t theme.Theme) ([]byte, error)
}
// allCards is the ordered list rendered by RenderAll. Filenames are plain
// kebab-case — README authors embed them by name, not by lexicographic order.
var allCards = []Card{
profileCard{},
reposPerLanguageCard{},
mostCommitLanguageCard{},
statsCard{},
productiveCard{},
productiveWeekdayCard{},
contributionsCard{},
contributionsHeatmapCard{},
topStarredReposCard{},
streakCard{},
mostCommitLanguageAllTimeCard{},
productiveAllTimeCard{},
productiveWeekdayAllTimeCard{},
contributionsAllTimeCard{},
contributionsByYearCard{},
recordsCard{},
}
// RenderAll writes every card into outDir/<themeID>/.
func RenderAll(p *github.Profile, t theme.Theme, outDir string) error {
dir := filepath.Join(outDir, t.ID)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("mkdir %s: %w", dir, err)
}
for _, c := range allCards {
data, err := c.SVG(p, t)
if err != nil {
return fmt.Errorf("render %s: %w", c.Filename(), err)
}
path := filepath.Join(dir, c.Filename())
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
}
return nil
}
// Filenames lists every card's on-disk basename in render order.
func Filenames() []string {
out := make([]string, len(allCards))
for i, c := range allCards {
out[i] = c.Filename()
}
return out
}