Files
Artifacto/internal/server/metrics.go
dtoro 2b0ab836fe ui: grayscale palette, square corners throughout
Removes all indigo/emerald/amber/red accent colors — everything now
uses the slate ramp. Strips rounded corners from containers, cards,
buttons, inputs, badges; the only remaining rounded element is the
circular logo image itself.

Affordances preserved without color:
- Primary buttons (Publish/Log in/Unlock/Copy link flash): bg-slate-100
  on dark background for high contrast
- Secondary buttons (Copy/Open): bg-slate-800
- Destructive (Delete): outlined with border-slate-500 so it reads as
  different from secondary
- Error messages: uppercase "ERROR" prefix + left border accent
- Success flash / ready drop state: uppercase "PUBLISHED"/"READY" labels
  and solid border-slate-200 vs. the dashed/empty state
- Protected lock: amber badge swapped for a slate-100 badge overlay
- Chart: slate-200 line on slate-400/20 fill instead of indigo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:28:21 +02:00

137 lines
3.6 KiB
Go

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, `<svg viewBox="0 0 %d %d" class="w-full h-40 text-slate-400" xmlns="http://www.w3.org/2000/svg">`, w, h)
// axes
fmt.Fprintf(&b, `<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="currentColor" stroke-width="1"/>`,
padLeft, padTop, padLeft, padTop+innerH)
fmt.Fprintf(&b, `<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="currentColor" stroke-width="1"/>`,
padLeft, padTop+innerH, padLeft+innerW, padTop+innerH)
// horizontal gridline at max
fmt.Fprintf(&b, `<text x="%d" y="%d" font-size="10" fill="currentColor" text-anchor="end">%d</text>`,
padLeft-4, padTop+6, max)
fmt.Fprintf(&b, `<text x="%d" y="%d" font-size="10" fill="currentColor" text-anchor="end">0</text>`,
padLeft-4, padTop+innerH)
n := len(series)
if n == 0 {
b.WriteString(`</svg>`)
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, `<path d="%s" fill="rgb(148 163 184 / 0.2)" stroke="none"/>`, area.String())
fmt.Fprintf(&b, `<polyline fill="none" stroke="rgb(226 232 240)" stroke-width="2" points="%s"/>`, 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, `<text x="%.1f" y="%d" font-size="10" fill="currentColor" text-anchor="middle">%s</text>`,
x, padTop+innerH+14, shortDay(series[i].Day))
}
b.WriteString(`</svg>`)
return b.String()
}
func shortDay(iso string) string {
// yyyy-mm-dd → mm-dd
if len(iso) >= 10 {
return iso[5:10]
}
return iso
}