package server import ( "fmt" "html/template" "net/http" "strings" "github.com/go-chi/chi/v5" "git.hubris.network/dtoro/artifacto/internal/store" ) type detailData struct { BaseURL string Artifact *store.Artifact Series []store.DayPoint Referrers []store.ReferrerCount ChartSVG template.HTML } func (s *Server) getArtifactDetail(w http.ResponseWriter, r *http.Request) { slug := chi.URLParam(r, "slug") art, err := s.store.GetArtifact(slug) if err != nil { http.NotFound(w, r) return } series, err := s.store.SeriesFor(slug, 30) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } refs, err := s.store.TopReferrers(slug, 10) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } s.render(w, "detail", detailData{ BaseURL: s.cfg.BaseURL, Artifact: art, Series: series, Referrers: refs, ChartSVG: template.HTML(renderChart(series)), }) } // renderChart produces a tiny SVG line+area chart with day labels. func renderChart(series []store.DayPoint) string { const w = 720 const h = 160 const padLeft = 30 const padBottom = 20 const padTop = 10 const padRight = 10 innerW := w - padLeft - padRight innerH := h - padTop - padBottom var max int64 for _, p := range series { if p.Views > max { max = p.Views } } if max < 1 { max = 1 } var b strings.Builder fmt.Fprintf(&b, ``, w, h) // axes fmt.Fprintf(&b, ``, padLeft, padTop, padLeft, padTop+innerH) fmt.Fprintf(&b, ``, padLeft, padTop+innerH, padLeft+innerW, padTop+innerH) // horizontal gridline at max fmt.Fprintf(&b, `%d`, padLeft-4, padTop+6, max) fmt.Fprintf(&b, `0`, padLeft-4, padTop+innerH) n := len(series) if n == 0 { b.WriteString(``) return b.String() } step := float64(innerW) / float64(maxInt(n-1, 1)) // build polyline path var pts strings.Builder for i, p := range series { x := float64(padLeft) + float64(i)*step y := float64(padTop+innerH) - float64(p.Views)/float64(max)*float64(innerH) if i > 0 { pts.WriteByte(' ') } fmt.Fprintf(&pts, "%.1f,%.1f", x, y) } // area under curve var area strings.Builder fmt.Fprintf(&area, "M%d,%d ", padLeft, padTop+innerH) for i, p := range series { x := float64(padLeft) + float64(i)*step y := float64(padTop+innerH) - float64(p.Views)/float64(max)*float64(innerH) fmt.Fprintf(&area, "L%.1f,%.1f ", x, y) } fmt.Fprintf(&area, "L%.1f,%d Z", float64(padLeft)+float64(n-1)*step, padTop+innerH) fmt.Fprintf(&b, ``, area.String()) fmt.Fprintf(&b, ``, pts.String()) // x-axis tick labels: first, middle, last day labelIdx := []int{0, n / 2, n - 1} for _, i := range labelIdx { if i < 0 || i >= n { continue } x := float64(padLeft) + float64(i)*step fmt.Fprintf(&b, `%s`, x, padTop+innerH+14, shortDay(series[i].Day)) } b.WriteString(``) return b.String() } func shortDay(iso string) string { // yyyy-mm-dd → mm-dd if len(iso) >= 10 { return iso[5:10] } return iso }