ui

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 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 it has no country to report and no database that could produce one.

It names the configuration because the configuration is the fix — and it is shown only where that is the whole truth. A link showing country rows in the window on screen gets those rows, not this sentence (F160).

**The scope is the link and the window, not the instance.** The caller's test is over one LinkStats breakdown, so on a database-less instance holding country history two links can differ: one with countries inside the selected window draws a map, the one beside it with none meets this sentence. That is narrower than F160's fix note claimed, and widening it needs a query this page deliberately does not make — see httpx.fillLinkAnalytics, and F195.

One constant, used by the map and by the ranked list, because the two views of **one** link's window must not be able to disagree about whether a country can be reported for it. TestTheMapAndTheRankedListUseOneSentence asserts they use it — the name written here until 2026-08-10 was of no test in the tree.

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 QRThumbClass = "h-24 w-24"

QRThumbClass is the class the link page's QR thumbnail carries, and it is exported because the drawing is generated outside this package (M48).

**The box has to be stated in a class rather than left to the picture.** The thumbnail sits in the link page's heading row, in front of the destination box that M47 measured at 327px from the top of a 1280×800 viewport and that M48 re-measured at 349px with this picture beside it, and `internal/qr` sizes an `<svg>` from the encoded version — a longer URL is a bigger code, so the `width` and `height` attributes are a function of the data. A class is not: 6rem is 6rem for every link in the product, which is what makes the height of the heading row something the markup states rather than something the data decides. TestTheEditControlIsReachableWithoutScrolling is the rule that requires it, and decisions.md carries why it is a rule and not an exemption.

Both dimensions, because a QR code is square and a CSS height alone would leave the `width` attribute to fight it.

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 is the top of the axis: a rounded ceiling nobody observed, which is
	// what makes the gridlines readable. It is **not** a reading, and labelling
	// it as one is the defect F164 records — a 30-day maximum of 2,351 rendered
	// as "peak 5,000/day", 113% high, with nothing beside it to contradict the
	// number.
	MaxY int64
	// Peak is the largest reading in the series, which is the figure a reader
	// takes "peak" to mean. Zero for an empty series, like every other field
	// here.
	Peak  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 is the gridline, in the plot's own coordinates.
	Y int
	// LabelY is the baseline the label sits on, in the outer viewport's — see
	// series_chart.html for why the two are the same number of units and only
	// the labels escape the horizontal stretch. Text hangs above its baseline,
	// so the top gridline's label drops below the line and every other one sits
	// just above it; a label drawn at Y=0 renders outside the box entirely.
	LabelY int
	Label  string
	// BoxY, BoxW and BoxH are the chip the label is drawn on.
	//
	// Not decoration. A gridline label sits at the left of the plot and the
	// leftmost bar is often the tallest, so the figure lands on `fill-accent-
	// hover` — where `subtle` ink measures **1.66:1** in the light theme and
	// 3.08:1 in the dark, against a package that has never shipped text under
	// 4.5:1. The chip is `sunken`, which puts `muted` at 6.92:1 and 12.61:1.
	// All four are WCAG 2.x ratios computed from input.css's own hex values at
	// M58 — they read 1.82, 3.15, 6.84 and 12.0 until then, and every
	// conclusion drawn from them is unchanged.
	BoxY, BoxW, BoxH int
}

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 this view has no country to draw and no way to
	// acquire one — nothing resolved for the link and window the caller is
	// rendering, and no database — 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 a country can be reported for the link and window being drawn — passed in rather than inferred from an empty map, because a link with no clicks yet and a view with no country and no way to acquire one are different facts and only one of them is worth telling somebody about.

**It is not "is a GeoIP database configured".** That was the test until F160, and it hid the map from every instance holding country history whose database had never been configured or had since been removed — the demo among them. The caller decides; see httpx.fillLinkAnalytics, where the answer is this window's rows or the configuration, either one.

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