Help us shape the future of Go! Take the 2026 Go Developer Survey

admin

package
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 81 Imported by: 0

Documentation

Overview

Package admin is the server-rendered admin UI: html/template pages from backend/web, htmx for interactivity, the same session cookie and stores as the JSON API. It never imports internal/server; the server builds Deps from its services (server/admin_mount.go) and calls Mount.

Index

Constants

View Source
const (
	BackupRunning = "running"
	BackupOK      = "ok"
	BackupFailed  = "failed"

	BackupManual    = "manual"
	BackupPreUpdate = "pre_update"
)

Backup states and kinds as the page sees them. They mirror the engine's values; the admin does not import the engine, so the server adapter maps.

View Source
const DefaultAdminPath = "/admin"

DefaultAdminPath is where the admin lives unless Deps.AdminPath says otherwise. It is the one place the prefix is spelled out.

Variables

View Source
var (
	// ErrBackupRunning is returned by Start when a backup is already running.
	ErrBackupRunning = errors.New("a backup is already running")
	// ErrBackupNotFound is returned for an id that is not a backup.
	ErrBackupNotFound = errors.New("backup not found")
)
View Source
var (
	ErrPluginNotFound = errors.New("plugin not found")
	ErrBadGrant       = errors.New("permission not requested by the plugin")
	// ErrCommunityGrant is a permission a community plugin may not be given.
	ErrCommunityGrant = errors.New("permission not allowed for a community plugin")
)

ErrPluginNotFound, ErrBadGrant and ErrCommunityGrant are what PluginAdmin.Grant answers.

View Source
var ErrAddressRefused = errors.New("admin address refused")

ErrAddressRefused means the address was reserved, invalid, or weak without an acknowledgement. Nothing was stored and the old address keeps serving.

View Source
var ErrNoUpdate = errors.New("there is no update to install")

ErrNoUpdate is Apply's answer when the stored status names nothing to install.

View Source
var ErrThemeActive = errors.New("theme is active")

ErrThemeActive is what Uninstall answers for the active theme.

View Source
var ErrWrongCode = errors.New("admin: wrong second-factor code")

ErrWrongCode is what Verify returns for a code that is definitely wrong. Only this error counts toward the per-session cap. Any other error means the provider could not decide (an outage, a timeout) and is not counted.

Functions

func BuilderPalette

func BuilderPalette(cfg *siteconfig.Config) (colors, space, fs map[string]string)

BuilderPalette is the resolved palette of the site config for callers outside the admin (the MCP builder_design tool): colours by name, spacing by step and font sizes by name.

func CheckDangerPhrase

func CheckDangerPhrase(r *http.Request, dc ui.DangerConfirm) (ui.DangerConfirm, bool)

CheckDangerPhrase is the server side of ui.DangerConfirm. It compares the posted confirm_phrase to dc.Phrase exactly: no trimming, no case folding, and an empty phrase never matches. On a mismatch it returns dc with Error set and false; the screen re-renders its page with that card and status 422. The browser's button state is only a hint, so every screen must call this.

func IsHTMX

func IsHTMX(r *http.Request) bool

IsHTMX reports an htmx-issued request .

func Nonce

func Nonce(ctx context.Context) string

Nonce returns the CSP nonce of the request ("" outside admin routes).

func Redirect

func Redirect(w http.ResponseWriter, r *http.Request, target string)

Redirect sends the browser to target after a successful action: a 303 for plain requests, HX-Redirect (a full navigation) for htmx ones.

func SafeNextFor

func SafeNextFor(prefix, raw string) string

SafeNextFor validates a post-login "next" target: a same-site path under the admin prefix only. Anything else (absolute URLs, //host, backslashes, control chars, other prefixes) becomes "" so the caller falls back to the default landing page.

The sudo page is never a target, but its own next is: a sign-in from an expired sudo page lands on the page the owner was headed to. That unwraps at most twice.

func Toast

func Toast(w http.ResponseWriter, kind, message string)

Toast sets the HX-Trigger header that makes admin.js show a toast.

Types

type APIKeyStore

type APIKeyStore interface {
	List(ctx context.Context) ([]*auth.APIKey, error)
	Create(ctx context.Context, createdBy *uuid.UUID, in *auth.CreateAPIKeyInput) (*auth.APIKeyWithSecret, error)
	Revoke(ctx context.Context, id uuid.UUID, revokedBy *uuid.UUID) error
	Regenerate(ctx context.Context, id uuid.UUID) (string, error)
	Delete(ctx context.Context, id uuid.UUID) error
}

APIKeyStore is auth.APIKeyService as the API-keys screen uses it.

type AddressBanner

type AddressBanner struct {
	Path                 string
	ChooseHref, KeepHref string
	// KeepLabel is "Keep <address>…".
	KeepLabel string
}

AddressBanner is the owner notice for a weak admin address nobody accepted.

type AddressLock

type AddressLock string

AddressLock says why the admin address cannot be changed from the admin.

const (
	// AddressUnlocked: an owner may change the address.
	AddressUnlocked AddressLock = ""
	// AddressLockedEnv: the server environment pins the address.
	AddressLockedEnv AddressLock = "env"
	// AddressLockedDemo: the public demo does not allow changes.
	AddressLockedDemo AddressLock = "demo"
)

type AddressService

type AddressService interface {
	State() AddressState
	// Assess ranks a candidate against the reserved names and the site.
	Assess(ctx context.Context, path string) adminpath.Result
	// Generate returns a new random strong candidate. It stores nothing.
	Generate() string
	// Apply stores req.Path and serves the admin there from now on.
	Apply(ctx context.Context, req ApplyRequest) (ApplyResult, error)
	// Keep records that an owner knowingly accepted the current address.
	Keep(ctx context.Context, by string) error
}

AddressService is the admin's view of the live admin address. The server implements it: changing the address rebuilds the admin and swaps it in.

type AddressState

type AddressState struct {
	Path    string
	Level   adminpath.Level
	Reasons []adminpath.Reason
	// SetBy and SetAt are nil for an address the server generated itself and
	// for installs that predate the record.
	SetBy string
	SetAt *time.Time
	// AckBy and AckAt are set when an owner knowingly accepted a weak address.
	AckBy string
	AckAt *time.Time
	Lock  AddressLock
	// Pinned is the environment variable name behind AddressLockedEnv. It is
	// data from the server so no template spells the address or its source.
	Pinned string
}

AddressState is what the admin shows about the live admin address. The service keeps it cached, so reading it costs no database round trip.

type Admin

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

Admin holds the parsed templates and assets and the HTTP handlers.

func New

func New(d Deps) (*Admin, error)

New parses every template and bundles the assets. A parse failure is returned so `serve` refuses to boot rather than 500 on every page.

func (*Admin) AdminPath

func (a *Admin) AdminPath() string

AdminPath is the URL prefix this instance is mounted under.

func (*Admin) Error

func (a *Admin) Error(w http.ResponseWriter, r *http.Request, status int, heading, message string)

Error answers with an error page, or, for an htmx fragment request, a toast plus HX-Reswap: none so the current page stays put.

func (*Admin) ErrorBack

func (a *Admin) ErrorBack(w http.ResponseWriter, r *http.Request, status int, heading, message, back, backLabel string)

ErrorBack is Error with a way back that suits the screen the error is on.

func (*Admin) Mount

func (a *Admin) Mount(r *httpx.Group)

Mount registers every /admin route on r. Registration order is irrelevant: ServeMux picks the most specific pattern.

func (*Admin) Nav

func (a *Admin) Nav() *Nav

Nav returns the sidebar registry (plugins register their items on it).

func (*Admin) NewView

func (a *Admin) NewView(w http.ResponseWriter, r *http.Request, title string) *View

NewView builds the root view for a request: chrome data from the request context and the site config, and the flash message (consumed here).

func (*Admin) Render

func (a *Admin) Render(w http.ResponseWriter, r *http.Request, status int, page string, v *View)

Render writes page with status. Normal requests get the whole document; an htmx request targeting a region gets only the page's "main" block. A template error becomes a 500 page, never a half-written response.

func (*Admin) RenderPartial

func (a *Admin) RenderPartial(name string, data any) (template.HTML, error)

RenderPartial executes one named partial (tests, and handlers that return a fragment such as a table row after an htmx action).

func (*Admin) RequireProfile

func (a *Admin) RequireProfile(next http.Handler) http.Handler

RequireProfile sends a signed-in user whose person has no name or no photo to the complete-profile step. It runs after RequireSession and never writes: "complete" is derived from the person, so there is no flag to keep in step. A GET navigation carries next= so the step ends where the user was headed; an htmx request gets HX-Redirect (next is the page it came from); anything else (a POST) is redirected without next, since replaying it is not safe.

func (*Admin) RequireRole

func (a *Admin) RequireRole(roles ...string) func(http.Handler) http.Handler

RequireRole allows only the listed roles; others get the 403 page.

func (*Admin) RequireSession

func (a *Admin) RequireSession(next http.Handler) http.Handler

RequireSession authenticates the hatamoto_session cookie (independent of ENABLE_COOKIE_AUTH, which only gates the JSON API) and puts the user, their claims and the DB context on the request. Failures redirect to the login page (htmx: 401 + HX-Redirect so the whole page navigates). It never rotates the session: parallel htmx requests would race the rotation.

func (*Admin) RequireSudo

func (a *Admin) RequireSudo(next http.Handler) http.Handler

RequireSudo lets a request through only when its session confirmed the password in the last sudoTTL. Put it after RequireSession. Anyone else is sent to the confirmation page; a POST is never replayed, so the user submits the form again once confirmed.

func (*Admin) SetBackups

func (a *Admin) SetBackups(svc BackupsService)

SetBackups hands the admin the backup service once the engine is known: the server mounts the admin before it exists. Call it before serving starts.

func (*Admin) SetFlash

func (a *Admin) SetFlash(w http.ResponseWriter, r *http.Request, kind, message string)

SetFlash stores a message for the next page view (60 s, HMAC-signed so it cannot be forged into the page).

func (*Admin) SetSecondFactor

func (a *Admin) SetSecondFactor(f SecondFactor)

SetSecondFactor registers the provider sudo mode asks for a code. Pass nil to remove it. It is safe to call while the admin serves requests.

func (*Admin) SetStats added in v1.0.2

func (a *Admin) SetStats(svc StatsService)

SetStats sets the install stats service after New, like SetUpdates. Call it only with a non-nil service: a typed nil would defeat the nil checks.

func (*Admin) SetUpdates

func (a *Admin) SetUpdates(svc UpdatesService, unavailable string)

SetUpdates sets the updates service (or why there is none) after New. The server mounts the admin before the update agent exists; call it before the server starts serving.

type AgentStats

type AgentStats interface {
	agents.StatsStore
	FirstCrawls(ctx context.Context, rg agents.Range, p agents.Page) ([]agents.FirstCrawlRow, int, error)
	FirstCrawlAggregates(ctx context.Context, rg agents.Range) (agents.FirstCrawlAgg, error)
	PublishedPages(ctx context.Context) (int, error)
	FetchesBySchema(ctx context.Context, rg agents.Range) ([]agents.SeriesPoint, error)
	HealthSeries(ctx context.Context, rg agents.Range) ([]agents.HealthDay, error)
	NeverFetchedBySchema(ctx context.Context) ([]agents.SchemaCount, error)
	agents.RobotsStore
}

AgentStats is the read side of AI-crawler analytics (agents.StatsStore plus first crawls) and the robots policy.

type ApplyRequest

type ApplyRequest struct {
	Path string
	// By is the display name or email of the owner choosing it.
	By string
	// Ack is true when the owner typed the confirmation for a weak address.
	Ack bool
}

ApplyRequest asks for a new admin address. Path is already normalised.

type ApplyResult

type ApplyResult struct{ Path string }

ApplyResult is what a successful Apply reports; Path is the live address.

type Assets

type Assets struct {
	CSSURL    string
	Importmap template.HTML
	Tokens    []byte
	// contains filtered or unexported fields
}

Assets is the fingerprinted CSS/JS bundle plus the vendored scripts, built once at boot. All URLs live under <prefix>/_/ so the edge needs no rule.

func (*Assets) Handler

func (a *Assets) Handler() http.Handler

Handler serves GET <prefix>/_/{path...}.

func (*Assets) JSURL

func (a *Assets) JSURL(name string) string

JSURL is the fingerprinted URL of js/<name>.js ("" if unknown).

func (*Assets) Module

func (a *Assets) Module(spec string) (Preload, bool)

Module returns the URL and integrity of an import-map specifier.

func (*Assets) Preloads

func (a *Assets) Preloads(entries []string) ([]Preload, error)

Preloads returns the modulepreload closure of the entry modules (import-map specifiers such as "@hm/admin"), dependencies before dependents. An unknown entry is returned as an error so a typo in a template fails its test.

func (*Assets) Scripts

func (a *Assets) Scripts(extra []string) []ScriptTag

Scripts returns the classic vendored scripts for a page: the defaults plus the extra names the page asked for (e.g. "sortablejs").

type AuditEvent

type AuditEvent struct {
	ActorID, Actor, Action, Target string
	Detail                         map[string]any
}

AuditEvent is one owner action for the admin audit log. Detail must carry no secrets.

type AuditReader

type AuditReader interface {
	Recent(ctx context.Context, limit, offset int) ([]AuditRow, int, error)
	Latest(ctx context.Context, limit int) ([]AuditRow, error)
}

AuditReader lists stored audit events newest first. Recent also counts the stored events (for pagination); Latest skips that count for callers that only want the newest few.

type AuditRow

type AuditRow struct {
	ID                    int64
	At                    time.Time
	Actor, Action, Target string
	Detail                map[string]any
}

AuditRow is one stored audit event as the Audit log page and the Overview Activity card read it.

type Authenticator

type Authenticator interface {
	LoginSession(ctx context.Context, email, password, userAgent, ip string) (*user.User, string, error)
	IssueSession(ctx context.Context, userID uuid.UUID, userAgent, ip string) (string, error)
}

Authenticator signs people in (auth.Service).

type AuthorStore

type AuthorStore interface {
	List(ctx context.Context) ([]*person.Person, error)
	Resolve(ctx context.Context, name string) (entity.Byline, error)
}

AuthorStore feeds the author picker: the people a byline can credit, and the resolver behind "Or add a new author". A name that is the site organisation resolves to Org, never to a person.

type BackupInfo

type BackupInfo struct {
	ID           string
	Kind         string // BackupManual or BackupPreUpdate
	Target       string // the version a pre-update backup was taken for
	State        string // BackupRunning, BackupOK or BackupFailed
	Percent      int
	Error        string
	Created      time.Time
	Size         int64
	Parts        []string // archive members, e.g. core.dump, content.tar.gz
	Plugins      []string
	MediaMissing int // referenced media files that were not on disk
}

BackupInfo is one backup as the page shows it.

type BackupsService

type BackupsService interface {
	List(ctx context.Context) (BackupsState, error)
	// Start begins a manual backup in the background and returns its id.
	// It returns ErrBackupRunning when one is already running.
	Start(ctx context.Context, actor string) (string, error)
	// Open returns the archive for download. The caller closes it.
	Open(ctx context.Context, id string) (io.ReadSeekCloser, BackupInfo, error)
	// Delete removes a finished backup; ErrBackupNotFound for an unknown id.
	Delete(ctx context.Context, id string) error
}

BackupsService is the backup engine as the admin uses it. The server adapter implements it; Start never runs the backup on the request context.

type BackupsState

type BackupsState struct {
	Rows          []BackupInfo // newest first
	Running       bool
	FreeBytes     uint64 // free space where backups are written; 0 when unknown
	KeepManual    int
	KeepPreUpdate int
}

BackupsState is everything the page needs in one read.

type ConfirmInfo

type ConfirmInfo struct {
	Name                    string
	Downgrade, SignerChange bool
	// WidenedAccess is set when an update can call web addresses the installed
	// version could not; it also clears the plugin's grants.
	WidenedAccess          bool
	NewHosts               []string
	NewFetchAny            bool
	NewIsland              bool
	NewScripts             bool
	OldVersion, NewVersion string
	OldSigner, NewSigner   string
	// Missing are section types the active theme would lose.
	Missing []MissingSection
}

ConfirmInfo is what replacing an installed package puts at stake: the owner must confirm it. SignerChange and WidenedAccess also clear the plugin's grants.

type ContentDeps

type ContentDeps struct {
	Entities EntityStore
	Terms    TermStore
	Media    MediaStore
	Authors  AuthorStore
	// PublicPath is the public URL path of an entity ("" when unknown).
	PublicPath func(ctx context.Context, e *entity.Entity) string
	// PublicPaths is PublicPath for many entities of one schema at once (one
	// ancestor query for all of rows, none without nested URLs). routable is
	// false, with nil paths, when the schema has no public URL; rows may be nil
	// to ask only that. Nil falls back to PublicPath per entity.
	PublicPaths func(ctx context.Context, sc *entity.EntitySchema, rows []*entity.Entity) (paths []string, routable bool)
	// Theme lists the active theme's section types; nil (or empty) when page
	// rendering is off, which leaves the page editor without "Add section" and
	// without a preview.
	Theme func() []ThemeSection
	// Preview renders unsaved attributes: it answers a POST with the JSON body
	// of /api/v1/render/preview (entity_id, schema, attributes, path, canvas)
	// with the full HTML document. Nil turns the live preview off.
	Preview http.Handler
	// RenderNodes applies builder ops to a draft page doc and renders node
	// fragments for the canvas: it answers a POST with the JSON body of the
	// node render request (the preview body plus ops, render, css, seq).
	// Nil turns the in-place canvas patching off (the editor reloads the frame).
	RenderNodes http.Handler
	// Templates are the section templates; nil hides saving and inserting them.
	Templates TemplateStore
	// Crawlers reads AI-crawler activity for the blog editor's "AI crawlers"
	// card; nil hides the card.
	Crawlers CrawlerStore
	// Recovery keeps the unsaved work of a published post or page per user, so
	// it can be offered back after a closed tab. Nil turns recovery copies
	// off: the routes answer 204 and the editors get no copy.
	Recovery RecoveryStore
}

ContentDeps are the services behind the entity, schema, taxonomy and media screens. Any may be nil only in route-table builds.

type CrawlerStore

type CrawlerStore interface {
	EntityActivity(ctx context.Context, entityID uuid.UUID, agentIDs []string, today time.Time) (agents.EntityActivityData, error)
}

CrawlerStore reads AI-crawler activity for one entity (agents.StatsRepository).

type Crumb

type Crumb = ui.Crumb

Crumb is one breadcrumb.

type CurrentUser

type CurrentUser struct {
	ID    string
	Name  string
	Email string
	Role  string
}

CurrentUser is the signed-in user as templates see them.

func UserFromContext

func UserFromContext(ctx context.Context) (CurrentUser, bool)

UserFromContext is the signed-in user of an admin request (false outside one).

func (*CurrentUser) Initials

func (u *CurrentUser) Initials() string

Initials is the avatar text: the first letters of the first and last word of the name ("Hana Sato" gives "HS"), the first two letters of a single word, "?" for an empty name.

type Deps

type Deps struct {
	Logger *slog.Logger
	// Sessions, Users and Site may be nil only in route-table builds: the
	// handlers are never invoked there.
	Sessions SessionStore
	Users    UserReader
	Site     SiteStore
	Nav      *Nav
	Auth     Authenticator
	Tokens   TokenService
	Finder   UserFinder
	Mailer   Mailer
	// BaseURL returns the site address the owner or operator configured
	// (PUBLIC_BASE_URL, else the Site address setting); "" when none is set.
	// Emailed links use only this, never the request's headers.
	BaseURL func(*http.Request) string
	// CookieSecure is ADMIN_COOKIE_SECURE: a floor for the Secure cookie
	// attribute (TLS seen on the request also turns it on).
	CookieSecure bool
	Manager      UserManager
	Passwords    PasswordChanger
	APIKeys      APIKeyStore
	// Webhooks is the outgoing-webhook service behind Settings > Webhooks; nil
	// answers that section with 404. Set it only when non-nil: a typed nil
	// would defeat the check.
	Webhooks WebhookStore
	// WebhookDeliveryOn is true when a job sends queued webhooks; false shows
	// the "delivery is switched off" notice.
	WebhookDeliveryOn bool
	// WebhooksPluginLeft says a plugin named webhooks is still installed (one
	// that was not moved into core); true shows the notice on the webhooks page.
	WebhooksPluginLeft func(context.Context) bool
	// Invalidate clears the rendered-page cache after settings change.
	Invalidate func(context.Context)
	// BuilderTokens reports whether the live theme opted in to the design
	// tokens (theme.json builder.tokens); nil means no.
	BuilderTokens func() bool
	// BuilderParts lists the layout slots (header, footer, single, archive) the
	// live theme renders parts for (theme.json builder.parts); nil means none.
	BuilderParts func() []string
	// PageCache is the rendered-page cache behind Settings > Performance;
	// nil answers that page with 503.
	PageCache PageCacheService
	// Spam is the spam protection settings behind Settings > Forms > Spam
	// protection; nil answers that page with 503.
	Spam SpamService
	// EmailSettings is the SMTP configuration behind Settings > Email and the
	// setup wizard's Email step; nil answers those pages with 503.
	EmailSettings EmailSettingsService
	// Updates is the automatic-update service behind Settings > Updates and
	// the owner banner; nil renders that page as unavailable and no banner.
	// Set it only when non-nil: a typed nil would defeat the check.
	Updates UpdatesService
	// Stats is the opt-in install stats service behind the Install stats card
	// on Settings > Updates, the setup wizard step and the owner banner; nil
	// hides all three. Set it only when non-nil.
	Stats StatsService
	// UpdatesUnavailable is why Updates is nil, shown on the page.
	UpdatesUnavailable string
	// Backups is the backup service behind Settings > Backups; nil answers
	// that page with 503. Set it only when non-nil.
	Backups BackupsService
	// Audit records an owner action in the admin audit log; nil skips it.
	Audit func(ctx context.Context, e AuditEvent)
	// AuditLog reads the audit log back; nil leaves the Audit log page
	// unmounted. Set it only when non-nil.
	AuditLog AuditReader
	Owners   OwnerCreator
	Starter  StarterInstaller
	// SetupCode gates the first owner; nil disables the wizard's first step.
	SetupCode *SetupCode
	// NeedsSetup reports a fresh install (no users yet); /admin/login then
	// sends visitors to the setup wizard. Nil means never.
	NeedsSetup func(context.Context) bool
	// FlashKey signs the flash cookie (derive it from the server secret).
	FlashKey []byte
	// SiteNameFallback is shown until the site config has a name.
	SiteNameFallback string
	// RateLimit wraps the unauthenticated auth screens; nil means none.
	RateLimit func(http.Handler) http.Handler
	// Growth holds the services of the menus, forms, redirects, dashboards,
	// linking and plugins screens (see growth.go).
	Growth GrowthDeps
	// Content holds the services behind the entity, schema, taxonomy and media
	// screens (v1-11).
	Content ContentDeps
	// People is the person behind each user account: the profile pages and
	// the sign-in profile gate. Nil skips the gate.
	People PeopleStore
	// PersonPath is the public path of a person by slug (the person content
	// type's URL pattern); nil or "" falls back to /author/{slug}.
	PersonPath func(ctx context.Context, slug string) string
	// Import runs the WordPress import wizard (importjob.Manager); nil answers 503.
	Import ImportService
	// Dir, when set, replaces the embedded templates/CSS/JS with files from
	// disk and rebuilds them per request (dev loop, HATAMOTO_ADMIN_DIR).
	Dir string
	// PublicAPIPrefix is the extra public API prefix (PUBLIC_API_PREFIX), empty
	// when only /api/v1/public is served. Form addresses shown in the admin use it.
	PublicAPIPrefix string
	// Dev turns on the development-only pages (the styleguide, HATAMOTO_DEV=1).
	Dev bool
	// Demo is the public demo mode (HATAMOTO_DEMO=1): a fixed banner, the
	// public login on the sign-in page and the refusals of config.DemoConfig.
	Demo config.DemoConfig

	// AdminPath is the URL prefix every admin route is served under. Empty
	// means DefaultAdminPath; otherwise it is "/" plus one lowercase [a-z0-9-]
	// segment of 1 to 64 characters, not starting or ending with "-". Reserved
	// names and the spec's length rules come in PR 2. Demo mode requires the
	// default.
	AdminPath string
	// Address is the live admin address: its state, the picker's checks and
	// the change itself. Nil hides the address screens (route-table builds).
	Address AddressService
	// Sudo, when set, holds the sudo grants. The server builds one and passes
	// it to every Admin it swaps in, so an owner keeps sudo across a change.
	// Nil means this Admin keeps its own.
	Sudo *SudoStore
}

Deps is everything the admin needs. Interfaces are declared here (not in the services) so handler tests run on fakes without a database.

type EmailInput

type EmailInput struct {
	Provider    string
	SESRegion   string
	Host        string
	Port        int
	Security    email.Security
	Username    string
	Password    string
	FromAddress string
	FromName    string
}

EmailInput is one submitted SMTP form after validation. Host is already derived for the SES and Gmail presets. Password is empty when the owner left it blank (see keepPassword on Check and Save).

type EmailSettingsService

type EmailSettingsService interface {
	Get(ctx context.Context) (EmailSettingsView, error)
	// Check connects and signs in with in without sending a message. A
	// *email.CheckError explains a failure. keepPassword uses the saved
	// password instead of in.Password.
	Check(ctx context.Context, in EmailInput, keepPassword bool) error
	// Save stores in (sealing the password) and applies it at once.
	// keepPassword keeps the saved password instead of in.Password.
	Save(ctx context.Context, in EmailInput, keepPassword bool) error
	// Reset deletes the saved settings so the server's SMTP_* apply again.
	Reset(ctx context.Context) error
}

EmailSettingsService is the SMTP configuration saved in the admin and applied to the running mail service without a restart.

type EmailSettingsView

type EmailSettingsView struct {
	// Saved reports whether a settings row exists. The form fields below are
	// defaults (custom, 587, STARTTLS) when it does not.
	Saved       bool
	Provider    string
	SESRegion   string
	Host        string
	Port        int
	Security    email.Security
	Username    string
	FromAddress string
	FromName    string

	// PasswordSaved: a password is stored and can be read.
	PasswordSaved bool
	// PasswordLost: a password is stored but cannot be read (the server
	// SECRET changed); the owner must type it again.
	PasswordLost bool

	Source EmailSource
	// Configured: the live service can send.
	Configured bool
	// Disabled: EMAIL_DISABLED=true switches all sending off.
	Disabled bool
}

EmailSettingsView is what the Email tab and wizard step show: the saved values (never the password) and how the live configuration was chosen.

type EmailSource

type EmailSource string

EmailSource says where the live email configuration comes from.

const (
	// EmailSourceSaved: settings saved in Settings > Email.
	EmailSourceSaved EmailSource = "saved"
	// EmailSourceServer: the server's SMTP_* environment.
	EmailSourceServer EmailSource = "server"
	// EmailSourceNone: nothing usable is configured.
	EmailSourceNone EmailSource = "none"
)

type EntityNavEntry

type EntityNavEntry struct {
	ID, Href, Label string
	Count           int
	Current         bool
}

EntityNavEntry is one link of the Entities subnav. ID is the entity type's slug and the no-JS select's option value: empty for "All entities" (never a valid slug, so it selects the unfiltered list).

type EntityStore

type EntityStore interface {
	ListSchemas(ctx context.Context) ([]*entity.EntitySchema, error)
	GetSchema(ctx context.Context, id uuid.UUID) (*entity.EntitySchema, error)
	CreateSchema(ctx context.Context, in *entity.CreateSchemaInput) (*entity.EntitySchema, error)
	UpdateSchema(ctx context.Context, id uuid.UUID, in *entity.UpdateSchemaInput) (*entity.EntitySchema, error)
	DeleteSchema(ctx context.Context, id uuid.UUID) error

	GetEntity(ctx context.Context, id uuid.UUID) (*entity.Entity, error)
	CreateEntity(ctx context.Context, userID *uuid.UUID, in *entity.CreateEntityInput) (*entity.Entity, error)
	UpdateEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, in *entity.UpdateEntityInput) (*entity.Entity, error)
	UpdateEntityStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status entity.EntityStatus) (*entity.Entity, error)
	ScheduleEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, at time.Time) (*entity.Entity, error)
	DeleteEntity(ctx context.Context, id uuid.UUID) error
	ListEntitiesWithOffset(ctx context.Context, f *entity.ListEntitiesFilter, p *entity.OffsetPage) (*entity.OffsetPagedResult[*entity.Entity], error)
	CountEntitiesBySchemaStatus(ctx context.Context) ([]entity.SchemaStatusCount, error)
	CountEntitiesByParent(ctx context.Context, limit int) ([]entity.ParentCount, error)
	CountEntityChildren(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)
	// RefUsers lists what embeds a block (a block's usage list).
	RefUsers(ctx context.Context, blockID uuid.UUID, limit int) (users []entity.RefUser, capped bool, err error)

	ListVersions(ctx context.Context, id uuid.UUID, limit int) ([]*entity.EntityVersion, error)
	GetVersion(ctx context.Context, id uuid.UUID, version int) (*entity.EntityVersion, error)
	RestoreVersion(ctx context.Context, id uuid.UUID, userID *uuid.UUID, version int) (*entity.Entity, error)
}

EntityStore is entity.Service as the entity and schema screens use it.

type Flash

type Flash struct {
	Kind    string `json:"k"` // success | error | warning | info
	Message string `json:"m"`
	Expires int64  `json:"e"`
	Inline  bool   `json:"-"` // stay beside the form instead of becoming a toast
}

Flash is a one-shot message carried across a redirect.

type FormMailer

type FormMailer interface {
	Configured() bool
	Forward(ctx context.Context, f *form.Form, sub *form.FormSubmission, to []string, by string) error
}

FormMailer forwards a submission by email (the "Email this" action).

type FormStore

type FormStore interface {
	ListForms(ctx context.Context, status string) ([]*form.Form, error)
	GetForm(ctx context.Context, id uuid.UUID) (*form.Form, error)
	CreateForm(ctx context.Context, in *form.CreateFormInput) (*form.Form, error)
	UpdateForm(ctx context.Context, id uuid.UUID, in *form.UpdateFormInput) (*form.Form, error)
	DeleteForm(ctx context.Context, id uuid.UUID) error
	ListSubmissions(ctx context.Context, formID uuid.UUID, p *form.ListSubmissionsParams) ([]*form.FormSubmission, int, error)
	GetSubmission(ctx context.Context, id uuid.UUID) (*form.FormSubmission, error)
	UpdateSubmission(ctx context.Context, id uuid.UUID, in *form.UpdateSubmissionInput) (*form.FormSubmission, error)
	DeleteSubmission(ctx context.Context, id uuid.UUID) error
}

FormStore is form.Service as the forms screens use it.

type GrowthDeps

type GrowthDeps struct {
	Menus     MenuStore
	Forms     FormStore
	Redirects RedirectStore
	Explain   URLExplainer
	Pickers   Pickers
	Sections  SectionStore
	Agents    AgentStats
	Referrals Referrals
	Linking   LinkingStore
	Plugins   PluginAdmin
	Packages  PackageAdmin
	FormMail  FormMailer
	// AgentRegistry names the crawlers (agents.Default() when nil).
	AgentRegistry *agents.Registry
}

GrowthDeps groups the services of the site and growth screens.

type ImportService

type ImportService interface {
	Begin(ctx context.Context, actor importjob.Actor, src importjob.Source) (uuid.UUID, error)
	Get(ctx context.Context, id uuid.UUID) (*importjob.Run, error)
	Recent(ctx context.Context, limit int) ([]importjob.Run, error)
	Start(ctx context.Context, id uuid.UUID, actor importjob.Actor, host string, o importjob.Options) error
	Resume(ctx context.Context, id uuid.UUID, actor importjob.Actor, host string, cred *importjob.Source) error
	Cancel(ctx context.Context, id uuid.UUID, actor importjob.Actor) error
}

ImportService is importjob.Manager as the WordPress import wizard uses it. Nothing on it can lift the engine's private-host guard: that is a flag of the command line only.

type InsertOutcome

type InsertOutcome struct {
	Inserted   bool
	SkipReason string
}

InsertOutcome reports what Insert did.

type LinkingStore

type LinkingStore interface {
	ListRules(ctx context.Context, f *linkingv2.RuleFilter) (*linkingv2.PaginatedRulesResult, error)
	GetRule(ctx context.Context, id uuid.UUID) (*linkingv2.LinkRule, error)
	CreateRule(ctx context.Context, in *linkingv2.CreateRuleInput) (*linkingv2.LinkRule, error)
	UpdateRule(ctx context.Context, id uuid.UUID, in *linkingv2.UpdateRuleInput) (*linkingv2.LinkRule, error)
	DeleteRules(ctx context.Context, ids []uuid.UUID) (int, error)
	DeleteRulesBySource(ctx context.Context, source string) (int, error)
	DeleteAllRules(ctx context.Context) (int, error)
	PreviewRule(ctx context.Context, phrase string, target uuid.UUID) (linkingv2.RulePreviewResult, error)
	Suggest(ctx context.Context, source uuid.UUID) ([]linkingv2.Suggestion, error)
	Equity(ctx context.Context) (*linkingv2.EquityReport, error)
	Inbound(ctx context.Context, target uuid.UUID) ([]linkingv2.EquityDetailRow, error)
	// EntityName is the display name of an entity, "" when it does not exist.
	EntityName(ctx context.Context, id uuid.UUID) string
	// Insert wraps the first usable mention of anchor in source's content with a
	// link to target (the suggestion panel's "Insert").
	Insert(ctx context.Context, source, target uuid.UUID, anchor string) (InsertOutcome, error)
	// SuggestIn is Suggest against the editor's live (unsaved) content html.
	SuggestIn(ctx context.Context, source uuid.UUID, html string) ([]linkingv2.Suggestion, error)
	// WrapIn links the first usable mention of anchor in html to target and
	// returns the new html. Nothing is stored: the editor saves it as usual.
	WrapIn(ctx context.Context, target uuid.UUID, anchor, html string) (string, InsertOutcome, error)
}

LinkingStore is internal linking v2 as the linking screens use it.

type Mailer

type Mailer interface {
	IsConfigured() bool
	SendPasswordReset(ctx context.Context, to string, data email.ResetMailData) error
	SendInvite(ctx context.Context, to string, data email.InviteMailData) error
	SendTest(ctx context.Context, to, siteName string) error
	SendAdminAddressChanged(ctx context.Context, to string, data email.AdminAddressChangedData) error
	SendAdminAddressSetup(ctx context.Context, to string, data email.AdminAddressSetupData) error
}

Mailer sends the auth emails (email.Service).

type MediaStore

type MediaStore interface {
	Upload(ctx context.Context, userID *uuid.UUID, file io.Reader, filename, mimeType string, size int64) (*media.Media, error)
	Get(ctx context.Context, id uuid.UUID) (*media.Media, error)
	List(ctx context.Context, f *media.ListMediaFilter, limit, offset int) (*media.PagedResult, error)
	Update(ctx context.Context, id uuid.UUID, in *media.UpdateMediaInput) (*media.Media, error)
	Delete(ctx context.Context, id uuid.UUID) error
	Usage(ctx context.Context, m *media.Media) ([]media.Usage, error)
	Config() media.Config
}

MediaStore is media.Service as the library and the picker use it.

type MenuStore interface {
	ListMenus(ctx context.Context) ([]*menu.Menu, error)
	GetMenuWithItems(ctx context.Context, id uuid.UUID) (*menu.Menu, error)
	CreateMenu(ctx context.Context, in *menu.CreateMenuInput) (*menu.Menu, error)
	ReplaceMenu(ctx context.Context, id uuid.UUID, fields *menu.UpdateMenuInput, items []menu.ReplaceItem) (*menu.Menu, error)
	DeleteMenu(ctx context.Context, id uuid.UUID) error
}

MenuStore is menu.Service as the menu screens use it.

type MissingSection

type MissingSection struct {
	Type string
	Uses int
}

MissingSection is a section type the content uses that a theme lacks.

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

Nav is the sidebar registry. Core registers its six primary items; none has children (plugin pages live in the Plugins subnav).

func (n *Nav) AddProvider(f func() []NavItem)

AddProvider registers a function whose items join the registered ones on every render.

func (n *Nav) For(role, path string) []NavEntry

For resolves the primary entries visible to role and marks the one covering path. The most specific prefix wins, so /admin/plugins lights up Plugins and not Settings.

func (n *Nav) Register(it NavItem)

Register adds an item (a duplicate ID replaces the earlier one).

type NavEntry struct {
	Label, Href, Icon, Badge string
	Current                  string
	Group                    string
}

NavEntry is a NavItem resolved for one user and path. Current is the aria-current value: "page" on the link of the page, "" otherwise.

type NavGroup struct {
	Label string
	Items []NavEntry
}

NavGroup is a run of consecutive entries under one title ("" for none).

func GroupNav

func GroupNav(entries []NavEntry) []NavGroup

GroupNav folds consecutive entries with the same Group into NavGroups. A group with no visible entry never appears, since it is derived from entries. Entries sharing a Group must be adjacent (items need adjacent Order).

type NavItem struct {
	ID, Label, Href, Icon string
	Order                 int
	// Group is the sidebar section title; consecutive items sharing one are
	// listed together, and "" means no title. Items sharing a Group must have
	// adjacent Order, or GroupNav splits them into separate groups.
	Group string
	Roles []string
	// Prefixes are the paths the item covers: it is the current section when
	// the request path equals one or lies below it (at a "/" boundary). Empty
	// means Href alone.
	Prefixes []string
	// Exact limits the item to Href alone: Overview lives at /admin, which is
	// the parent of every other admin path.
	Exact bool
	// Badge is evaluated per render ("" hides it).
	Badge func() string
	// Visible, when set, decides visibility per role instead of Roles.
	Visible func(role string) bool
	// HrefFor, when set, resolves the link per role instead of Href.
	HrefFor func(role string) string
}

NavItem is one sidebar link. Roles empty means every signed-in user.

type OwnerCreator

type OwnerCreator interface {
	CreateFirstOwner(ctx context.Context, name, email, password string) (*user.User, error)
}

OwnerCreator creates the first owner (auth.Bootstrap).

type PackageAdmin

type PackageAdmin interface {
	Registry(ctx context.Context) RegistryView
	// Fetch downloads kind/name from the registry (hash-checked).
	Fetch(ctx context.Context, kind, name string) ([]byte, error)
	// Inspect parses a package for the permission prompt.
	Inspect(ctx context.Context, data []byte) (PackageSummary, error)
	// Install stores and applies the package; grant is the approved permission
	// list (nil keeps the earlier grant); confirm is the owner's yes to a
	// Summary.Confirm. Reasons the owner should read come back as *Refusal.
	Install(ctx context.Context, data []byte, source string, grant []string, confirm bool) error
	SetEnabled(ctx context.Context, name string, on bool) error
	Uninstall(ctx context.Context, kind, name string, dropData bool) error
	AllowUnsigned(ctx context.Context) bool
	SetAllowUnsigned(ctx context.Context, on bool) error

	Themes(ctx context.Context) ([]ThemeInfo, error)
	MissingSections(ctx context.Context, theme string) ([]MissingSection, error)
	// Activate switches the site to the theme ("default" is the built-in one).
	Activate(ctx context.Context, theme string) error
	ThemeSettingsSchema(ctx context.Context, theme string) []byte
	ThemeSettings(ctx context.Context, theme string) (map[string]any, error)
	SaveThemeSettings(ctx context.Context, theme string, values map[string]any) (schemaform.Errors, error)
	// PreviewURL is the public-site URL that starts a preview of the theme.
	PreviewURL(theme string) string

	// UIFile serves a file of the plugin package's ui/ directory; it reports
	// false when the plugin is not loaded or has no such file.
	UIFile(w http.ResponseWriter, r *http.Request, plugin, file, csp, cache string) bool
	// CallRoute runs one of the plugin's own routes as the signed-in user.
	CallRoute(ctx context.Context, plugin, method, path string, query url.Values, body []byte, user CurrentUser) RouteResult
	// CallHost runs one host function of the plugin (its permissions apply);
	// the result is the sdk.CallResponse JSON.
	CallHost(ctx context.Context, plugin, fn string, args json.RawMessage) json.RawMessage
}

PackageAdmin is the package manager as the Plugins and Themes pages use it.

type PackageMeta

type PackageMeta struct {
	Enabled bool
	// WidgetOnly is set for a package that ships widgets and no plugin code:
	// the runtime has no status for it, and that is healthy.
	WidgetOnly bool
	Signed     bool
	Signer     string
	// FirstParty is set when the package is signed by one of the project's own keys
	// (whether that key is still trusted is Blocked's business).
	FirstParty bool
	// WebhooksImported is set (webhooks row only) when the boot step has recorded that
	// the plugin's subscriptions were copied into core.
	WebhooksImported bool
	Source           string
	// Update is the newer version the registry offers ("" when none).
	Update string
	// UpdateNew lists the permissions that update asks for and the installed
	// version does not; empty when Update is, or when it asks for nothing new.
	UpdateNew []string
	// Blocked says why policy keeps the package from loading (a signer that is
	// no longer trusted, a dev package in production); "" when it is not.
	Blocked string
	// ScriptsBlocked is set when the plugin ships browser JavaScript (an
	// assets/plugin.js, or script tags for the page head) that is not loaded
	// because the owner has not granted site.scripts.
	ScriptsBlocked bool
	// MarkupStopped lists the sections and partials that are not registered
	// because they carry scripts and the plugin lacks site.scripts.
	MarkupStopped []string
	// AsksScripts: its manifest lists site.scripts, so the owner can grant it.
	AsksScripts bool
	// Uninstalling is set while an uninstall is unfinished (retry it).
	Uninstalling bool
}

PackageMeta is what the packages table knows about an installed plugin.

type PackageSummary

type PackageSummary struct {
	Kind, Name, Version, Description string
	Permissions, AllowedHosts        []string
	Signed                           bool
	Signer                           string
	// Problem says why installing would be refused (untrusted signer, bad
	// signature, unsigned while unsigned packages are off); "" when it would go ahead.
	Problem string
	// Updating is set when a package of that name is installed already.
	Updating         bool
	InstalledVersion string
	Granted          []string
	// Confirm is set when replacing the installed package needs the owner's
	// confirmation (older version, other signer, lost sections).
	Confirm *ConfirmInfo
}

PackageSummary is a package as shown on the permission prompt.

type PageAnswer

type PageAnswer struct {
	Kind     string // entity | home | archive | ""
	Name     string
	Schema   string
	Path     string // canonical path
	EntityID string
}

PageAnswer names what serves a public path.

type PageCacheService

type PageCacheService interface {
	Stats(ctx context.Context) PageCacheStats
	Settings(ctx context.Context) (PageCacheSettings, error)
	Save(ctx context.Context, s PageCacheSettings) error
	Clear(ctx context.Context) (pages int, err error)
	PreloadNow(ctx context.Context) (started bool, err error)
	PreloadStatus() PreloadStatus
}

PageCacheService is the rendered-page cache as Settings > Performance uses it. The server implements it and owns persistence; the admin only validates the form and calls through.

type PageCacheSettings

type PageCacheSettings struct {
	Enabled                bool
	TTLSeconds, MaxMB      int
	PreloadEnabled         bool
	PreloadIntervalSeconds int
	// PreloadMode is "missing" (skip cached pages) or "all" (refresh).
	PreloadMode string
	// PreloadAfterChanges schedules a preload shortly after content changes.
	PreloadAfterChanges bool
}

PageCacheSettings are the owner's stored choices. TTLSeconds 0 means "use the server default", MaxMB 0 means no limit, PreloadIntervalSeconds 0 means "same as the lifetime".

type PageCacheStats

type PageCacheStats struct {
	Pages                int
	Bytes, CapBytes      int64
	EffectiveTTL         time.Duration
	ServerDefaultTTL     time.Duration
	TTLSource            string
	ServerDefaultFromEnv bool
	BaseURLPinned        bool
	SchedulerAvailable   bool
}

PageCacheStats is the live state shown next to the form. TTLSource is "admin" or "server".

type PasswordChanger

type PasswordChanger interface {
	ChangePassword(ctx context.Context, userID uuid.UUID, current, newPassword string, keepSession *uuid.UUID) error
}

PasswordChanger changes a user's own password (auth.UserTokens).

type PeopleStore

type PeopleStore interface {
	// ForUser returns the person a user owns, person.ErrNotFound when none.
	ForUser(ctx context.Context, userID uuid.UUID) (*person.Person, error)
	// Save writes the profile form, making the person from the typed name when
	// the user has none; invalid input is a *person.ValidationError.
	Save(ctx context.Context, userID uuid.UUID, in person.ProfileInput) (*person.Person, error)

	// List returns every person by name.
	List(ctx context.Context) ([]*person.Person, error)
	// PostCounts is the number of published entities credited to each person.
	PostCounts(ctx context.Context) (map[uuid.UUID]int, error)
	// LinkedUsers maps a person to the user that owns it (people without a
	// login are absent).
	LinkedUsers(ctx context.Context) (map[uuid.UUID]uuid.UUID, error)
	// Create makes a person without a login; a bad name is a *person.ValidationError.
	Create(ctx context.Context, by *uuid.UUID, name string) (*person.Person, error)
	// MergePeople folds drop into keep (person.ErrMergeBothLogins when both
	// have a login).
	MergePeople(ctx context.Context, keepID, dropID uuid.UUID) (*person.MergeResult, error)
}

PeopleStore is *person.Profiles as the profile pages and the People screens use it. Nil only in route-table builds and in tests that do not touch people; the profile gate then lets every request through.

type PickOption

type PickOption struct {
	Value string // entity id, or a term's archive path
	Label string
	Hint  string // schema, slug, kind
	Slug  string // an entity's slug (empty for terms)
}

PickOption is one result of an entity or term search.

type Pickers

type Pickers interface {
	SearchEntities(ctx context.Context, q string, limit int) ([]PickOption, error)
	SearchTerms(ctx context.Context, q string, limit int) ([]PickOption, error)
	// EntityName is the display name of an entity, "" when it does not exist.
	EntityName(ctx context.Context, id uuid.UUID) string
}

Pickers search the things a menu item or a rule can point at. This is the minimal search-select backend; the entity and media pickers of v1-11 replace it.

type PluginAdmin

type PluginAdmin interface {
	Plugins() []PluginInfo
	Grant(ctx context.Context, name string, permissions []string) error
	// Settings is the stored settings with schema defaults merged and write-only
	// values masked.
	Settings(ctx context.Context, name string) (map[string]any, error)
	// StoredSecrets names the writeOnly settings that hold a value; the form
	// says "stored" for them and never shows the value.
	StoredSecrets(ctx context.Context, name string) (map[string]bool, error)
	// SaveSettings validates against the plugin's schema; problems come back as
	// field errors (dotted paths), not as an error.
	SaveSettings(ctx context.Context, name string, values map[string]any) (schemaform.Errors, error)
}

PluginAdmin is the plugin runtime as the plugins page uses it.

type PluginInfo

type PluginInfo struct {
	Name, Version, SDK, Description, Error, DataError string
	Loaded                                            bool
	Hooks, Permissions, Granted, AllowedHosts         []string
	// Refused is the part of Permissions this plugin may never be given (a
	// community plugin and its scripts); the page shows them unticked.
	Refused                      []string
	Routes, Cron, Cookies, Funcs []string // one human line each
	SettingsSchema               []byte
	// Package is set for a plugin installed from a package (nil for one loaded
	// from HATAMOTO_PLUGIN_DIR); UI for one that ships an admin page.
	Package *PackageMeta
	UI      *PluginUIInfo
}

PluginInfo is one plugin as the plugins page shows it.

type PluginNavEntry

type PluginNavEntry struct {
	ID, Href, Label, Marker string
	Muted, Current          bool
}

PluginNavEntry is one link of the Plugins subnav. ID is the no-JS select's option value: empty for the list (never a valid plugin name, so a plugin named "all" stays selectable) and the plugin's name otherwise.

type PluginUIInfo

type PluginUIInfo struct {
	Title string
	// Entry is the HTML file under the package's ui/ directory.
	Entry string
	Roles []string
	// Functions are the host functions its page may call: what its granted
	// permissions allow.
	Functions []string
}

PluginUIInfo is a plugin's declared admin page.

type Preload

type Preload struct {
	URL       string
	Integrity string
}

Preload is one <link rel="modulepreload"> of a page.

type PreloadFailure

type PreloadFailure struct {
	Path, Reason string
}

PreloadFailure names a page the last preload could not build and why.

type PreloadStatus

type PreloadStatus struct {
	Running       bool
	Done, Total   int
	HasResult     bool
	Finished      time.Time
	Pages, Errors int
	Duration      time.Duration
	StoppedEarly  bool
	Reason        string
	Skipped       int // pages already cached and left alone
	Failures      []PreloadFailure
}

PreloadStatus is the in-memory state of the preloader and its last result.

type RecoveryCopy

type RecoveryCopy = recovery.Copy

RecoveryCopy is one saved recovery copy (see internal/recovery).

type RecoveryStore

type RecoveryStore interface {
	Put(ctx context.Context, user, entity uuid.UUID, c RecoveryCopy) error
	// Get returns nil, nil when there is no copy.
	Get(ctx context.Context, user, entity uuid.UUID) (*RecoveryCopy, error)
	Delete(ctx context.Context, user, entity uuid.UUID) error
}

RecoveryStore keeps recovery copies, one per (user, entity).

type RedirectStore

type RedirectStore interface {
	ListRedirects(ctx context.Context, p redirect.RedirectListParams) ([]*redirect.Redirect, int, error)
	GetRedirect(ctx context.Context, id uuid.UUID) (*redirect.Redirect, error)
	CreateRedirect(ctx context.Context, in *redirect.CreateRedirectInput) (*redirect.Redirect, error)
	UpdateRedirect(ctx context.Context, id uuid.UUID, in *redirect.UpdateRedirectInput) (*redirect.Redirect, error)
	DeleteRedirect(ctx context.Context, id uuid.UUID) error
	List404Entries(ctx context.Context, p redirect.NotFoundListParams) ([]*redirect.NotFoundEntry, int, error)
	ListRecent404Entries(ctx context.Context, since time.Time, limit int) ([]*redirect.NotFoundEntry, error)
	Ignore404Entry(ctx context.Context, id uuid.UUID) error
	Delete404Entry(ctx context.Context, id uuid.UUID) error
}

RedirectStore is redirect.Service as the redirect screens use it.

type Referrals

type Referrals interface {
	AIReferrals(ctx context.Context, from, to time.Time) ([]analytics.ReferralDay, error)
	Pageviews(ctx context.Context, from, to time.Time) ([]analytics.DayCount, error)
}

Referrals reads the daily AI-referral and pageview rollup.

type Refusal

type Refusal struct{ Msg string }

Refusal is a package that was refused for a reason the owner should read (bad signature, unsigned, unknown permission, a theme that does not render).

func (*Refusal) Error

func (e *Refusal) Error() string

type RegistryEntry

type RegistryEntry struct {
	Kind, Name, Version, Description, Homepage string
	Permissions                                []string
	Installed                                  bool
	InstalledVersion                           string
	// Update is set when the registry's version is newer than the installed one.
	Update bool
}

RegistryEntry is one package the registry lists.

type RegistryView

type RegistryView struct {
	Entries []RegistryEntry
	// Off means no registry is configured; Err says why a configured one failed.
	Off bool
	Err string
	// Host is the registry's host name, shown in the empty state when Err is set.
	Host string
}

RegistryView is what browsing the registry returned.

type RouteResult

type RouteResult struct {
	Status      int
	ContentType string
	Body        []byte
}

RouteResult is a plugin route's response, relayed by the bridge.

type ScriptTag

type ScriptTag struct {
	URL       string
	Integrity string
}

ScriptTag is one classic vendored script with its subresource-integrity hash.

type SecondFactor

type SecondFactor interface {
	// Has reports whether the user enrolled a second factor. Return an error
	// when you cannot tell: sudo then refuses rather than skip the code.
	Has(ctx context.Context, userID uuid.UUID) (bool, error)
	// Verify checks the code the user typed. Return ErrWrongCode (or an error
	// wrapping it) for a wrong code, and any other error when you cannot tell.
	// Core only caps wrong codes per session (a handful per 15 minutes). A
	// provider should also count and lock its own failures per user, because a
	// session cap does not stop an attacker who opens new sessions.
	Verify(ctx context.Context, userID uuid.UUID, code string) error
}

SecondFactor is the seam a second-factor provider (an authenticator app, say) plugs into. Core ships none: without one, sudo asks for the password only.

type SectionStore

SectionStore is sectiontemplate.Service as the section template screens use it.

type SessionStore

type SessionStore interface {
	Validate(ctx context.Context, token string) (*session.Session, error)
	Revoke(ctx context.Context, id uuid.UUID) error
	RevokeAllForUser(ctx context.Context, userID uuid.UUID) error
}

SessionStore is the slice of session.Store the admin uses.

type SettingsSection

type SettingsSection struct {
	ID, Label, Icon, Desc, Href string
	// Group is the sidebar heading the section sits under (Site, Content,
	// System); empty for areas that show no headings.
	Group string
	Tabs  []SettingsTab
}

SettingsSection is one entry of the Settings secondary sidebar. Href is the first tab the role may open.

type SettingsTab

type SettingsTab struct {
	ID, Label, Href string
	Roles           []string
}

SettingsTab is one page of a Settings section. Roles empty means every signed-in user, exactly as the route behind it is gated.

type SetupCode

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

SetupCode gates the creation of the first owner: whoever reaches a fresh install first must still prove access to the server (the code is in its log, or in HATAMOTO_SETUP_CODE). Only a hash is kept in memory.

func NewSetupCode

func NewSetupCode() (*SetupCode, string)

NewSetupCode generates a random 8-character code and returns it for the log line ("XXXX-XXXX"); the SetupCode keeps only its hash.

func SetupCodeFrom

func SetupCodeFrom(code string) *SetupCode

SetupCodeFrom wraps an operator-chosen code (HATAMOTO_SETUP_CODE).

func (*SetupCode) Verify

func (c *SetupCode) Verify(in string) bool

Verify compares in constant time, ignoring case, spaces and dashes.

type SiteStore

type SiteStore interface {
	Get(ctx context.Context) (*siteconfig.Config, error)
	Update(ctx context.Context, in siteconfig.UpdateInput) (*siteconfig.Config, error)
}

SiteStore reads and writes the singleton site config.

type SpamService

type SpamService interface {
	Settings(ctx context.Context) (spam.Settings, error)
	Save(ctx context.Context, s spam.Settings) error
	SealSecret(secret string) ([]byte, error)
	// SecretReadable reports whether the saved secret can be opened.
	SecretReadable(s spam.Settings) bool
	// TestSecret checks the saved secret with the provider.
	TestSecret(ctx context.Context, s spam.Settings) (valid bool, detail string, err error)
}

SpamService is the spam protection settings and the provider check behind Settings > Forms > Spam protection. The captcha secret only ever goes in (SealSecret) and is checked against the provider (TestSecret); it never comes back out.

type StarterInstaller

type StarterInstaller interface {
	Install(ctx context.Context, o starter.Options) (starter.Result, error)
}

StarterInstaller installs the optional starter content.

type StatsBanner added in v1.0.2

type StatsBanner struct {
	PreviewHref string // Settings > Updates, at the card that shows what is sent
}

StatsBanner is the owner's one-time question about install stats on an install that has not answered it yet.

type StatsService added in v1.0.2

type StatsService interface {
	// View is the stored choice, whether the server configuration forces it
	// off, and the exact payload the daily ping would send.
	View(ctx context.Context) (StatsView, error)
	// Unanswered reports whether the owner still has to answer: the server
	// configuration does not turn stats off and the choice is unset. It must
	// be cheap, the banner asks it on every owner page.
	Unanswered(ctx context.Context) (bool, error)
	// ForcedOff reports HATAMOTO_TELEMETRY=off. It reads no state.
	ForcedOff() bool
	// Choose stores the owner's answer. Yes creates the install ID, No (or
	// switching off) deletes it.
	Choose(ctx context.Context, yes bool) error
	// Reset replaces the install ID with a new one.
	Reset(ctx context.Context) error
}

StatsService is the opt-in install stats as Settings > Updates, the setup wizard and the owner banner use it. The server implements it on top of the installstats package; the admin only validates forms and calls through.

type StatsView added in v1.0.2

type StatsView struct {
	Consent     string     // "unset", "yes" or "no"
	ForcedOff   bool       // HATAMOTO_TELEMETRY=off: the owner cannot change it
	PreviewJSON string     // the exact request body
	LastSent    *time.Time // time of the last successful report
}

StatsView is what the admin shows about install stats.

type SudoStore

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

SudoStore is the sudo-mode grants of a process. Share one between Admin values to keep elevations when the admin is rebuilt (the address change swaps in a new Admin). Use NewSudoStore.

func NewSudoStore

func NewSudoStore() *SudoStore

NewSudoStore returns an empty store.

type TemplateStore

type TemplateStore interface {
	List(ctx context.Context, f *sectiontemplate.ListFilter, limit, offset int) (*sectiontemplate.ListResult, error)
	Get(ctx context.Context, id uuid.UUID) (*sectiontemplate.SectionTemplate, error)
	Create(ctx context.Context, userID *uuid.UUID, in *sectiontemplate.CreateInput) (*sectiontemplate.SectionTemplate, error)
	RecordUsage(ctx context.Context, id uuid.UUID) error
}

TemplateStore is sectiontemplate.Service as the page editor uses it: saving a section as a template and inserting one.

type TermStore

type TermStore interface {
	ListTerms(ctx context.Context, kind, search string, page, perPage int) (*taxonomy.TermListResult, error)
	GetTerm(ctx context.Context, id uuid.UUID) (*taxonomy.Term, error)
	CreateTerm(ctx context.Context, in taxonomy.CreateTermInput) (*taxonomy.Term, error)
	UpdateTerm(ctx context.Context, in taxonomy.UpdateTermInput) (*taxonomy.Term, error)
	DeleteTerm(ctx context.Context, id uuid.UUID) error
	MergeTerms(ctx context.Context, from, into uuid.UUID) error
	ListTermsForEntity(ctx context.Context, entityID uuid.UUID) ([]*taxonomy.Term, error)
	ReplaceEntityTerms(ctx context.Context, entityID uuid.UUID, termIDs []uuid.UUID) error
}

TermStore is taxonomy.Service as the taxonomy screens and the term picker use it.

type ThemeInfo

type ThemeInfo struct {
	Name, Version, Description string
	Builtin, Active            bool
	Signed                     bool
	Signer, Source             string
	HasSettings                bool
	Update                     string
	// Blocked says why policy keeps an installed theme from running ("" when it is not).
	Blocked string
}

ThemeInfo is one theme on the Themes page.

type ThemeSection

type ThemeSection struct {
	Type, Title, Description string
	Schema                   json.RawMessage
}

ThemeSection is one section type of the active theme: what the page editor offers in "Add section" and builds the section form from.

type TokenService

type TokenService interface {
	IssueReset(ctx context.Context, userID uuid.UUID) (string, error)
	ResetValid(ctx context.Context, token string) bool
	ConsumeReset(ctx context.Context, token, newPassword string) (*user.User, error)
	PeekInvite(ctx context.Context, token string) (*auth.Invite, error)
	ConsumeInvite(ctx context.Context, token, name, password string) (*user.User, error)
	IssueInvite(ctx context.Context, email string, role user.Role, invitedBy uuid.UUID) (string, *auth.Invite, error)
	ListInvites(ctx context.Context) ([]auth.Invite, error)
	RevokeInvite(ctx context.Context, id uuid.UUID) error
}

TokenService is the reset/invite token flow (auth.UserTokens).

type URLExplainer

type URLExplainer interface {
	ExplainURL(ctx context.Context, path string) (URLExplanation, error)
}

URLExplainer resolves a path the way the public site does.

type URLExplanation

type URLExplanation struct {
	Redirects *redirect.Answer
	// Page is what serves the path when no redirect fires; Kind is "" when
	// nothing does (a 404).
	Page PageAnswer
}

URLExplanation answers "what happens to this URL": the redirect providers in order (the first match wins) and, when none redirects, the page that serves it.

type UpdateBanner

type UpdateBanner struct {
	Kind    string // rolled_back | blocked | backup_failed
	Message string
	Href    string
	Label   string
}

UpdateBanner is the owner-only notice in the admin shell.

func PickBannerFor

func PickBannerFor(prefix string, in UpdatesBannerInput, _ time.Time) *UpdateBanner

PickBannerFor returns the single most urgent banner, or nil. Order: rolled back, blocked, backup failed: only what needs the owner's action. A major release and updates being off for 90+ days are told by the top-bar chip. A blocked update is not announced while updates are off (the page says "Updates are off"). A dev build never gets one.

func (*UpdateBanner) CalloutKind

func (b *UpdateBanner) CalloutKind() string

CalloutKind is the hm/callout kind that styles the banner.

func (*UpdateBanner) DismissKey

func (b *UpdateBanner) DismissKey() string

DismissKey is the session-scoped dismissal key; a danger banner (a rollback or a failed backup) is never dismissible because it is an error the owner has to act on.

type UpdateChip

type UpdateChip struct {
	ID      string // the popover's element id
	Version string // "v1.4.2", or "dev build"
	Title   string // "Up to date", "v1.4.3 is ready", "v1.4.3 rolled back"
	Channel string // "Stable channel", "" on a dev build
	Note    string // one sentence under the title (a major release, updates off for 90+ days)
	Name    string // the trigger's accessible name
	Blocks  []UpdateChipBlock
	Rows    []UpdateChipRow
	Href    string
	Label   string
}

UpdateChip is the owner's always-visible update status in the top bar: a few tiny release blocks, the running version and a popover with the latest releases. Built from the cached UpdatesGlance, never from View.

type UpdateChipBlock

type UpdateChipBlock struct {
	Kind string // success | danger | info | neutral
}

UpdateChipBlock is one tiny release block, oldest first.

type UpdateChipRow

type UpdateChipRow struct {
	Text  string // "Updated to v1.4.2"
	When  string
	Badge string // badge kind
}

UpdateChipRow is one release switch in the popover.

type UpdatesBannerInput

type UpdatesBannerInput struct {
	Status  UpdatesStatus
	Mode    string
	Running string
	Dev     bool
}

UpdatesBannerInput is what PickBanner needs: the stored status and the effective mode.

type UpdatesForm

type UpdatesForm struct {
	Mode, Channel          string
	WindowStart, WindowEnd *int
	DeferDays              int
}

UpdatesForm is the policy an owner submitted. Locked fields are ignored by the service. A nil hour means "any time".

type UpdatesGlance

type UpdatesGlance struct {
	Running  string
	Dev      bool
	Mode     string
	Channel  string
	Status   UpdatesStatus
	Applying bool // an Update now run is in flight; read live, never cached
	OffSince *time.Time
	History  []UpdatesHistoryRow // newest first, at most 20
}

UpdatesGlance is the small, cached subset of UpdatesState the chip needs.

type UpdatesHistoryRow

type UpdatesHistoryRow struct {
	When     time.Time
	From, To string
	Phase    string
	Error    string
}

UpdatesHistoryRow is one release switch the supervisor made.

type UpdatesLocks

type UpdatesLocks struct {
	Mode, Channel       bool
	ModeVar, ChannelVar string
}

UpdatesLocks marks fields the server's environment sets, which the page shows read-only and a post cannot change.

type UpdatesResult

type UpdatesResult struct {
	Status  UpdatesStatus
	Target  string // Apply: the version being installed
	Started bool   // Apply: a background run was started
	Busy    bool   // another check or update is already running
}

UpdatesResult reports a Check or an Apply.

type UpdatesService

type UpdatesService interface {
	// View is everything the page shows: running version, effective policy,
	// env locks, stored status and history.
	View(ctx context.Context) (UpdatesState, error)
	// Save stores the policy. It returns field errors (keyed by the form
	// element id: mode, channel, window-start, defer) when the updater rejects
	// a value, and ignores locked fields.
	Save(ctx context.Context, in UpdatesForm) (map[string]string, error)
	// Check fetches and decides without downloading anything.
	Check(ctx context.Context) (UpdatesResult, error)
	// Apply starts an update in the background to the target the service
	// derives from its own stored status (ErrNoUpdate when there is none).
	// The caller never names a version.
	Apply(ctx context.Context) (UpdatesResult, error)
	// ApplyWithoutBackup is Apply for an update the pre-update backup
	// stopped: the same run, with the backup skipped for this version only.
	ApplyWithoutBackup(ctx context.Context) (UpdatesResult, error)
	// Unpin clears the stored pin.
	Unpin(ctx context.Context) error
	// Banner is the owner banner, nil when nothing needs attention.
	Banner(ctx context.Context) (*UpdateBanner, error)
	// Glance is the short, cached read behind the top-bar chip and the
	// Overview: it never writes and never costs more than one read per cache
	// window, so every owner page may ask for it.
	Glance(ctx context.Context) (UpdatesGlance, error)
}

UpdatesService is the automatic-update engine as Settings > Updates and the owner banner use it. The server implements it on top of the updater; the admin only validates forms and calls through. Every method that changes something goes through the same updater path as the scheduled run.

type UpdatesState

type UpdatesState struct {
	Running     string
	Dev         bool // not a release build: updates are disabled
	Mode        string
	Channel     string
	Channels    []string
	WindowStart *int
	WindowEnd   *int
	DeferDays   int
	Pin         string
	Locks       UpdatesLocks
	Status      UpdatesStatus
	Applying    bool // an update started from here is still running
	OffSince    *time.Time
	History     []UpdatesHistoryRow
}

UpdatesState is the Updates page data. Mode, Channel and the window are the effective values (environment over stored over defaults).

func (UpdatesState) ApplyIsMajor

func (s UpdatesState) ApplyIsMajor() bool

ApplyIsMajor reports that Update now would install a major release, which asks for a confirmation first: it can change how data is stored and the site cannot go back past it.

func (UpdatesState) ApplyTarget

func (s UpdatesState) ApplyTarget() string

ApplyTarget is what "Update now" would install on this install: nothing on a dev build or while the environment locks updates off (an operator's switch-off is not overridden from the page), else the status target.

func (UpdatesState) ApplyWithoutBackupTarget

func (s UpdatesState) ApplyWithoutBackupTarget() string

ApplyWithoutBackupTarget is the version "Update without a backup" installs: the target of an update the pre-update backup stopped, "" in any other state. Like ApplyTarget it comes from the stored status, never a form.

func (UpdatesState) Current

func (s UpdatesState) Current() UpdatesStatus

Current is the status the page shows: a dev build and an off mode override whatever the last stored check said, and a failed check shows as such.

type UpdatesStatus

type UpdatesStatus struct {
	LastCheckAt    *time.Time
	LastResult     string // ok | error | skipped | ""
	Decision       string // one of the updater's decisions, "" before the first check
	Reason         string
	Target         string
	AvailableMajor string
}

UpdatesStatus is the outcome of the last update check.

func (UpdatesStatus) ApplyTarget

func (st UpdatesStatus) ApplyTarget(running string) string

ApplyTarget is the version "Update now" installs: a major the owner has to start by hand, a scheduled or notified release that skips the ramp, window and defer, or the target of an Update now that did not finish (so it can be tried again). "" when there is nothing to apply. The service derives the target from its stored status with this and never from the form.

type UserFinder

type UserFinder interface {
	GetByEmail(ctx context.Context, email string) (*user.User, error)
	Count(ctx context.Context) (int, error)
}

UserFinder looks users up by address (forgot-password) and counts them.

type UserManager

type UserManager interface {
	GetByID(ctx context.Context, id uuid.UUID) (*user.User, error)
	List(ctx context.Context, limit, offset int) ([]*user.User, error)
	Create(ctx context.Context, in *user.CreateUserInput, passwordHash string) (*user.User, error)
	Update(ctx context.Context, id uuid.UUID, in *user.UpdateUserInput) (*user.User, error)
	Delete(ctx context.Context, id uuid.UUID) error
}

UserManager is the user repository the Users screens use.

type UserReader

type UserReader interface {
	GetByID(ctx context.Context, id uuid.UUID) (*user.User, error)
}

UserReader loads the signed-in user.

type View

type View struct {
	Title    string
	Crumbs   []Crumb
	SiteName string
	// SiteURL is the public site the sidebar's "Open site" button opens.
	SiteURL string
	Lang    string
	Theme   string
	Path    string

	Nonce   string
	CSRF    string
	Assets  *Assets
	Scripts []string // extra vendored scripts, e.g. "sortablejs"
	// AreaCarry is the query the secondary sidebar's links and phone select
	// keep (the analytics range and compare switch); empty elsewhere.
	AreaCarry url.Values
	// Modules are the page's entry modules beyond "@hm/admin", declared by the
	// page template's "modules" block; Render fills them in. The head preloads
	// their static import closure.
	Modules []string
	Flash   *Flash
	User    *CurrentUser
	Nav     []NavEntry // flat; breadcrumbs read it, the sidebar uses NavGroups
	// Section and Tab place the page in the Settings frame (IDs from
	// settingsRegistry); empty means the page is not a Settings page.
	Section, Tab string

	Wide bool // bare layout: wider panel
	// Width is the page column in the shell: "narrow" (one readable form
	// column), "compact" (the same column, kept at that width inside settings),
	// "wide" (tables, grids, charts; the default) or "full"
	// (editors). Render normalises anything else to "wide".
	Width string
	// Refresh, in seconds, reloads the page without JavaScript (a live status
	// page); htmx pages poll instead.
	Refresh int
	// NoSiteAddress asks the owner to set the site address (emails need it).
	NoSiteAddress bool
	// SetupPending reminds the owner that the wizard was not finished.
	SetupPending bool
	// SkipMarkupHooks keeps the markup scan from adding modules: a page whose
	// markup carries hooks it must not load code for (the builder shell, whose
	// no-JS outline sits in <noscript>) declares what it needs itself.
	SkipMarkupHooks bool
	// UpdateBanner is the owner's update notice (nil when nothing needs
	// attention); NewView fills it for owner GET requests off the Updates page.
	UpdateBanner *UpdateBanner
	// UpdateChip is the owner's top-bar update status (nil for editors and
	// sites without an update service); NewView fills it for GET requests.
	UpdateChip *UpdateChip
	// AddressBanner nags the owner about a weak admin address nobody accepted
	// (nil otherwise); NewView fills it for owner GET requests off the Security pages.
	AddressBanner *AddressBanner
	// StatsBanner asks the owner of an existing install about install stats
	// (nil otherwise); NewView fills it for owner GET requests off the Updates page.
	StatsBanner *StatsBanner
	// DemoBanner is the public demo notice ("" outside demo mode).
	DemoBanner string
	Data       any // page-specific data
	// contains filtered or unexported fields
}

View is the root template data of every page. Partials never read it implicitly: they take explicit dict arguments.

func (*View) AreaEntry

func (v *View) AreaEntry() string

func (*View) AreaID

func (v *View) AreaID() string

AreaID, AreaLabel and AreaEntry name the area of the sidebar.

func (*View) AreaLabel

func (v *View) AreaLabel() string

func (*View) AreaNav

func (v *View) AreaNav() []SettingsSection

AreaNav is the secondary sidebar of the signed-in user; the shell renders it around a page that set View.Section.

func (*View) BootModules

func (v *View) BootModules() []Preload

BootModules are the page's own screen modules (top-level "@hm/name" specifiers) as <script type="module"> tags. The nested editor modules ("@hm/editor/kit", "@hm/canvas/host") are only preloaded: the screen module imports them itself.

func (*View) CurrentSection

func (v *View) CurrentSection() *SettingsSection

CurrentSection is the section the page belongs to, if the role may see it.

func (*View) EntitiesNav

func (v *View) EntitiesNav() []EntityNavEntry

EntitiesNav is the Entities subnav the shell renders on the entity list (nil on every other page).

func (*View) EntitiesNavManage

func (v *View) EntitiesNavManage() bool

EntitiesNavManage reports whether the subnav links to the entity types settings page (owners only; editors cannot open it).

func (*View) HTMXConfig

func (v *View) HTMXConfig() string

HTMXConfig is the content of <meta name="htmx-config">: nonces for what htmx injects, no eval, no script tags, no history snapshots (stale CSRF / secrets).

func (*View) HasSubnav

func (v *View) HasSubnav() bool

HasSubnav reports whether the shell wraps the page in a secondary sidebar.

func (*View) NavGroups

func (v *View) NavGroups() []NavGroup

NavGroups is the sidebar: Nav folded into titled groups.

func (*View) PluginsNav

func (v *View) PluginsNav() []PluginNavEntry

PluginsNav is the Plugins subnav the shell renders on /admin/plugins* pages (nil elsewhere and for signed-out requests).

func (*View) Preloads

func (v *View) Preloads() []Preload

Preloads is the modulepreload list of the page: the static import closure of the admin entry plus the page's declared entry modules (see BootModules).

func (*View) SectionTabs

func (v *View) SectionTabs() []ui.Tab

SectionTabs are the link tabs of the current section (none when it has one).

func (*View) SpeculationRules

func (v *View) SpeculationRules() template.HTML

SpeculationRules is the template accessor for speculationRules.

type WebhookStore

type WebhookStore interface {
	List(ctx context.Context) ([]webhook.ListedSubscription, error)
	// Create stores the subscription and returns it with its new signing secret.
	Create(ctx context.Context, in webhook.Subscription) (webhook.Subscription, string, error)
	Get(ctx context.Context, id string) (*webhook.Subscription, error)
	Update(ctx context.Context, id string, p webhook.SubscriptionPatch) error
	SetActive(ctx context.Context, id string, active bool) error
	// RegenerateSecret replaces the signing secret and returns the new one.
	RegenerateSecret(ctx context.Context, id string) (string, error)
	Delete(ctx context.Context, id string) error
	// SendTest sends a test delivery now and returns its recorded outcome.
	SendTest(ctx context.Context, id string) (*webhook.DeliveryRow, error)
	// ListDeliveries is one page of the delivery log (subscriptionID "" = all)
	// and the total row count. Rows never carry the payload.
	ListDeliveries(ctx context.Context, subscriptionID string, limit, offset int) ([]webhook.DeliveryRow, int, error)
	// RetryDelivery re-queues a failed delivery; false means it had not failed.
	RetryDelivery(ctx context.Context, id string) (bool, error)
	// CountFailedSince is how many deliveries queued since the time have failed.
	CountFailedSince(ctx context.Context, since time.Time) (int, error)
}

WebhookStore is the webhook service as the Settings > Webhooks screens use it. A list never carries a signing secret.

Source Files

Directories

Path Synopsis
Package a11y is the no-browser accessibility checker run over every golden page and partial: structural rules on the HTML (names, landmarks, ids, headings, tables, dialogs, CSP-hostile attributes) plus contrast and CSS lint on the tokens.
Package a11y is the no-browser accessibility checker run over every golden page and partial: structural rules on the HTML (names, landmarks, ids, headings, tables, dialogs, CSP-hostile attributes) plus contrast and CSS lint on the tokens.
Package schemaform renders a JSON Schema as an HTML form and reads the form back into JSON.
Package schemaform renders a JSON Schema as an HTML form and reads the form back into JSON.
Package starter installs the optional starter content of the setup wizard: a home page with sections, About and Contact pages, two blog posts, header and footer menus and a contact form.
Package starter installs the optional starter content of the setup wizard: a home page with sections, About and Contact pages, two blog posts, header and footer menus and a contact form.
Package ui holds the typed shapes of the admin partials (web/admin/partials).
Package ui holds the typed shapes of the admin partials (web/admin/partials).

Jump to

Keyboard shortcuts

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