ui

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package ui owns the dashboard's templates, static assets and rendering.

It deliberately depends on nothing but the standard library. Handlers live in internal/httpx and pass in whatever data a page needs, so this package can be tested by rendering a page and reading the HTML rather than by standing up services — and so a template change cannot quietly acquire a dependency on a service type.

Everything is embedded, because the deployment unit is one binary. There is no filesystem path to get wrong, no volume to forget to mount, and no way for the running instance to disagree with the templates it was built from.

Index

Constants

View Source
const GeoUnavailable = "Geographic data is unavailable: no GeoIP database is configured."

GeoUnavailable is the sentence a page shows when no GeoIP database is configured.

One constant, used by the map and by the ranked list, because the two views must not be able to disagree about whether this instance can resolve a country. TestCountryViewsAgreeWhenGeoIPIsAbsent asserts they use it.

View Source
const MailValueLimit = 512

MailValueLimit is the longest an interpolated value may be, in runes.

Truncation rather than refusal: a display name of ten thousand characters is somebody probing, and a mail that arrives with the name cut short is a better outcome than one that never arrives at all. Long enough that no legitimate name, address or URL this product generates comes close.

View Source
const StylesheetPath = "css/app.css"

StylesheetPath is the asset a build is expected to have generated.

Variables

This section is empty.

Functions

This section is empty.

Types

type Bar

type Bar struct {
	X, Y, W, H int
	Day        string
	Clicks     int64
	Visitors   int64
	Bots       int64
}

type Chart

type Chart struct {
	W, H  int
	PlotH int
	MaxY  int64
	Bars  []Bar
	Ticks []Tick
	First string // label of the first day, for the x axis
	Last  string
}

Chart is a bar chart laid out in Go rather than in the browser.

The dashboard's charts are server-rendered SVG. A charting library would be the only piece of custom JavaScript in the product, for four rectangles and an axis — and the CSP disallows inline styles, which most of them generate. Computing integer geometry here keeps the template a dumb loop.

func BarChart

func BarChart(points []DayCount, w, h int) Chart

BarChart lays out a day series in a w×h viewBox.

Exported for its geometry tests; templates reach it through the func map.

type DayCount

type DayCount struct {
	Day      string // 2006-01-02
	Clicks   int64
	Visitors int64
	Bots     int64
}

DayCount is one day of a click series as the chart helpers consume it.

A local type rather than analytics.DayPoint, because this package depends on nothing outside the standard library. Handlers convert; the conversion is three assignments and it keeps a template change from ever pulling a service package into the UI.

type DimensionSlice added in v0.2.0

type DimensionSlice struct {
	Name  string
	Count int64
}

DimensionSlice is the shape DonutChart consumes.

A local type for the same reason DayCount is one: this package depends on nothing outside the standard library, and a handler converting two fields is cheaper than the UI importing the analytics package.

type Donut added in v0.2.0

type Donut struct {
	// Size is the square viewBox side. Geometry is absolute inside it.
	Size     int
	Segments []DonutSegment
	Total    int64
	// Empty is true when there is nothing to draw, so the template can say so
	// rather than render a ring of nothing.
	Empty bool
}

Donut is a ring chart laid out in Go (M37).

The other half of "richer charts for the other dimensions". A ranked list answers "how many from Chrome"; it does not answer "is this link's traffic one browser or five", which is the question a share chart exists for and the one a column of numbers is worst at.

A ring rather than a pie because the hole is where the total goes, and a total in the middle is what stops somebody reading a 60% slice as a big number when it is 60% of nine clicks.

func DonutChart added in v0.2.0

func DonutChart(items []DimensionSlice, total int64, size int) Donut

DonutChart lays a breakdown out as a ring.

Segments are ordered largest first and shaded darkest first, which makes the colour encode rank rather than identity. That is deliberate: a categorical palette would need a token per category and would put "Chrome" and "Safari" in colours that mean nothing, whereas the ramp says "this one is bigger" in the same visual language the map beside it uses.

type DonutSegment added in v0.2.0

type DonutSegment struct {
	Path  string
	Class string
	Label string
	Value int64
	Share int
}

DonutSegment is one slice, already turned into a path.

type MapBand added in v0.2.0

type MapBand struct {
	Class string
	// Upper is the largest figure that falls in this band.
	Upper int64
}

MapBand is one step of the legend.

type MapShape added in v0.2.0

type MapShape struct {
	Path string
	// Class is the fill utility, already resolved. A template cannot build
	// `fill-choro-{{.Step}}` and have Tailwind find it, so the whole string
	// comes from Go — which is why this file is a @source in input.css, exactly
	// as funcs.go is for the status badges.
	Class string
	// Title is the shape's accessible name and its hover text: the country, and
	// its exact figure. It is what keeps the map honest about a five-band scale.
	Title string
}

MapShape is one country as the template needs it.

type Renderer

type Renderer struct {
	// contains filtered or unexported fields
}

Renderer holds the parsed template set and the fingerprinted asset table.

Templates are parsed once at boot. A syntax error therefore fails startup rather than the first request that happens to reach that page, which is the difference between a deploy that refuses to come up and one that looks healthy until someone clicks the wrong tab.

func New

func New() (*Renderer, error)

New parses every template and fingerprints every static asset.

func (*Renderer) AssetURL

func (r *Renderer) AssetURL(name string) string

AssetURL returns the fingerprinted URL for an asset, or the plain path if the asset is absent — a missing stylesheet should 404 visibly rather than render as an empty href.

func (*Renderer) MailTemplates added in v0.2.0

func (r *Renderer) MailTemplates() []string

MailTemplates lists the parsed mail templates, for the test that renders every one of them.

func (*Renderer) MissingAssets

func (r *Renderer) MissingAssets() []string

MissingAssets reports expected assets that no build produced.

Returned rather than fatal: a stylesheet-less dashboard is ugly but working, and refusing to start would turn a forgotten build step into an outage. The caller logs it loudly at boot.

func (*Renderer) Pages

func (r *Renderer) Pages() []string

Pages lists the parsed page names, for the test that asserts every page renders.

func (*Renderer) Render

func (r *Renderer) Render(w http.ResponseWriter, status int, page string, data any) error

Render writes a full page.

Rendered into a buffer first. Executing straight to the ResponseWriter would commit a 200 and a half-written page the moment a template referenced a missing field, leaving the browser with truncated HTML and the operator with no error to look at.

func (*Renderer) RenderMail added in v0.2.0

func (r *Renderer) RenderMail(name string, data map[string]string) (subject, body string, err error)

RenderMail turns one template and its data into a subject and a plain-text body.

The data is map[string]string rather than a struct or an `any`, and that is the load-bearing choice: it lets this function neutralize every value on the way in. A struct would put the responsibility back on whoever wrote the template.

func (*Renderer) RenderPartial

func (r *Renderer) RenderPartial(w http.ResponseWriter, status int, page, block string, data any) error

RenderPartial writes one named block, for an HTMX swap.

The block is looked up in the page's own template set, so a partial can use anything that page defines. Same buffering rule as Render.

func (*Renderer) StaticHandler

func (r *Renderer) StaticHandler(prefix string) http.Handler

StaticHandler serves the embedded assets.

Served from memory with a strong ETag and a one-year max-age. The long lifetime is safe because every URL the templates emit carries a content fingerprint: a new build changes the URL, so nothing can be served stale. Requests without the fingerprint still work and still validate.

type Tick

type Tick struct {
	Y     int
	Label string
}

type WorldMap added in v0.2.0

type WorldMap struct {
	ViewBox  string
	FillRule string
	Shapes   []MapShape
	Legend   []MapBand

	// Metric is "clicks" or "visitors": which figure the shading is of.
	Metric string
	// MetricLabel names it in the heading.
	MetricLabel string
	// Caveat is the unique-visitor caveat, repeated **verbatim** whenever the
	// shading is of unique visitors and empty otherwise.
	//
	// Verbatim is the requirement, not a style note. Unique visitors are a
	// privacy-preserving estimate at daily resolution; shading a map by one
	// without carrying the sentence that says so would launder an estimate into
	// a fact, and a map is a great deal more persuasive than a table.
	Caveat string

	// Max is the largest per-country figure, which is what the bands are a
	// fraction of. Zero when nothing was resolved.
	Max int64
	// Countries is how many countries have a nonzero figure.
	Countries int
	// Unmapped lists codes with traffic that this map has no shape for.
	Unmapped []string

	// Unavailable is set when no GeoIP database is configured, and carries the
	// same sentence the ranked list has always used. The map is not rendered at
	// all in that state: a world drawn entirely in the no-data colour is a
	// picture of nothing that looks like a picture of something.
	Unavailable string
}

WorldMap is a country choropleth laid out in Go, ready for a dumb template loop.

Same idiom as BarChart, and for the same three reasons: a charting library would be the only custom JavaScript in the product, the CSP disallows the inline styles most of them generate, and geometry computed here means the template holds no arithmetic. The shapes themselves are generated Go source — see internal/ui/geo — so a dashboard request never parses a map file.

func Choropleth added in v0.2.0

func Choropleth(values map[string]int64, metric, caveat string, available bool) WorldMap

Choropleth lays a country breakdown out over the world map.

values is per alpha-2 code. available says whether this instance has a GeoIP database at all — passed in rather than inferred from an empty map, because a link with no clicks yet and an instance that cannot resolve a country are different facts and only one of them is worth telling somebody about.

Directories

Path Synopsis
geo
Package geo holds the world map as SVG path data, and lays a choropleth out over it.
Package geo holds the world map as SVG path data, and lays a choropleth out over it.
mapgen command
Command mapgen converts the vendored world-atlas TopoJSON into Go source.
Command mapgen converts the vendored world-atlas TopoJSON into Go source.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL