web

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: MIT Imports: 47 Imported by: 0

Documentation

Overview

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

templ: version: v0.3.1020

Package web serves the engine's dashboard.

SEAM 3 of 4, and the one that is easiest to get wrong. The other three seams are Go interfaces, which compose naturally. A rendered HTML page does not: you cannot wrap a template in middleware. If the layout hard-codes its own sidebar and header, an application built on top of the engine has exactly two options — fork the templates, or rewrite the dashboard — and forking breaks the rule that the engine must stay runnable on its own.

The fix is to make the chrome data rather than markup. Layout renders slots; a SlotProvider fills them. The engine ships a provider with single-owner defaults. A wrapping application supplies its own, adding an organisation switcher, a balance indicator, or extra navigation, without editing a single file in this package.

templ: version: v0.3.1020

templ: version: v0.3.1020

Index

Constants

View Source
const DefaultBrandName = "Yacht"

DefaultBrandName is this engine's own name.

Compared against rather than only assigned: the wordmark is this one word drawn, so it is shown when the name is still this one and replaced by plain text the moment a wrapping application sets its own.

View Source
const FlashCookie = "yacht_flash"

FlashCookie is where a pending message waits. Not HttpOnly-exempt and not readable by script for any good reason, so it is HttpOnly like the session.

View Source
const PageSize = 50

PageSize is how many rows a list shows at once.

Chosen to fill a screen and stop, rather than to be generous. The failure this exists to prevent is a page that renders every row a cluster can produce: the response gets large, the browser lays out thousands of nodes, and the one row somebody wanted is no easier to find than it was.

View Source
const SessionCookie = "yacht_session"

SessionCookie is the name of the cookie carrying a session token.

It matches account.DefaultCookieName because the session provider resolves requests by reading a cookie of that name: a mismatch would write a cookie nothing reads.

Variables

This section is empty.

Functions

func Account

func Account(d AccountData) templ.Component

Account is the page a person manages themselves from.

Separate from /settings, which is the install's facts — the owner, the cluster, the mail transport, the version — and is about the machine rather than about anybody. One prefix meaning two scopes is how a reader stops trusting either.

func Activity

func Activity(d ActivityData) templ.Component

Activity is the cross-app deployment feed: what changed recently, anywhere. The per-app history answers "what happened to this"; this answers "what happened", which is the question you have when something broke and you do not yet know where.

func AddNode

func AddNode(d AddNodeData) templ.Component

AddNode hands over the command that joins a machine to this cluster.

The command carries the join token in the clear, because that is what the person running it needs. Everything else on the page — including the stored settings summary — is built from cluster.Settings, which does not carry it, so the command is the only place it can appear.

func AddNodeNodes

func AddNodeNodes(d AddNodeData) templ.Component

AddNodeNodes is the confirmation half, polled while somebody runs the command on the machine.

func AppDeploymentsBody

func AppDeploymentsBody(d AppDetailData) templ.Component

func AppDetail

func AppDetail(d AppDetailData) templ.Component

AppDetail is the Railway-shaped view: a rail of every app on the left so you can move between them without going back to a list, and a tabbed pane on the right for the one you are looking at.

func AppList

func AppList(apps []app.App) templ.Component

---------------------------------------------------------------- apps list

func AppLogLines

func AppLogLines(d LogsData) templ.Component

AppLogLines is the polled body.

func AppLogs

func AppLogs(d LogsData) templ.Component

AppLogs is the log pane for one app.

func BranchOptions

func BranchOptions(d BranchData) templ.Component

BranchOptions is the list the picker swaps in.

Buttons rather than a datalist: a datalist cannot be styled, behaves differently in every browser, and gives no way to say why it is empty — which is the case that matters most here, because a private repository and a typo look identical from outside.

func Canvas

func Canvas(d CanvasData) templ.Component

Canvas draws a project's apps and what connects them.

Server-rendered: the layout is computed in Go and the edges are an SVG path list. There is no client-side graph library because there is no Node in this project, and a canvas that needs a build step to render is one a self-hoster cannot patch. Dragging is progressive — every card is a link first, and the script only adds the ability to move it.

func CheckMail

func CheckMail() templ.Component

CheckMail takes no data on purpose.

It carries neither the address nor anything derived from it, so the response to a registered address and to an unregistered one is the same bytes. A page that said "we sent a link to alice@example.com" would only need to say it once, for an address that exists, to become the disclosure this whole flow avoids.

func Cluster

func Cluster(d ClusterData) templ.Component

func CustomDomainList

func CustomDomainList(d NetworkingData) templ.Component

CustomDomainList is the polled half: every claim and how far it has got.

It renders the element carrying its own polling trigger, and the fragment endpoint returns this same component with hx-swap="outerHTML". That is what makes the poll self-stopping: once every domain has settled the replacement markup carries no trigger, and htmx has nothing left to fire. Swapping only the inside would leave the trigger on a wrapper it never replaces, asking forever.

Polled separately from the tab so a request every three seconds does not re-render the add-domain form under somebody typing a hostname into it.

func DeployLogPanel

func DeployLogPanel(d DeployLogsData) templ.Component

DeployLogPanel is one deployment's logs, shown in a sheet over the list.

func DirectoryOptions

func DirectoryOptions(d DirectoryData) templ.Component

DirectoryOptions is the listing the picker swaps in.

Each entry is the full path rather than the bare name, because the field holds a path and clicking something that put "cmd" in it while standing in "server" would be a control that lies about what it does.

func Events

func Events(d EventsData) templ.Component

Events is where Kubernetes explains itself. "Pending" is not actionable; "Insufficient cpu" or "ErrImagePull" is.

func FormatCPU

func FormatCPU(millis int64) string

FormatCPU turns millicores back into what the cluster is given.

Whole cores where it divides cleanly, because "2" is what somebody meant and "2000m" is the same thing said in a way that invites a second look.

func FormatMemory

func FormatMemory(mib int64) string

FormatMemory turns mebibytes back into what the cluster is given.

func Layout

func Layout(s Slots, page templ.Component) templ.Component

Layout renders the chrome around a page.

Note what this component does NOT do: it never reads an owner, a session, or anything about who is asking. Everything variable arrives as Slots. That is what lets a wrapping application change the entire look of the shell without touching this file.

func NewApp

func NewApp(d NewAppData) templ.Component

func NodeDetail

func NodeDetail(d NodeDetailData) templ.Component

NodeDetail is one machine, what is on it, and how to take it out of service.

func NodeDetailBody

func NodeDetailBody(d NodeDetailData) templ.Component

NodeDetailBody is the polled half: how far the drain has got, and what is still running.

func Overview

func Overview(d OverviewData) templ.Component

func PlatformDNS

func PlatformDNS(d PlatformDNSData) templ.Component

PlatformDNS is the install-wide DNS configuration, and every domain that depends on it.

func ProjectList

func ProjectList(d ProjectListData) templ.Component

ProjectList is every canvas a team has.

Shaped like the cluster page, because it answers the same kind of question: how much is here, and what are the pieces. The counts come first so the size of the install is readable without counting rows.

func RegistryPage

func RegistryPage(d RegistryData) templ.Component

RegistryPage is where images this install builds are pushed and pulled from.

func Settings

func Settings(d SettingsData) templ.Component

func SignIn

func SignIn(d SignInData) templ.Component

SignIn offers both ways in, to everyone, always.

Both forms are rendered unconditionally and with no knowledge of any address. Anything that decided what to show after learning whether an address has a password would be an enumeration oracle, and a worse one than "does this address exist" because it sorts real accounts by how they are configured. It does not matter whether such a probe is a round trip, a fetch or a status code: the only safe design is one where the server is never asked.

func TeamPage

func TeamPage(d TeamPageData) templ.Component

TeamPage lists who is in the team and who has been invited.

Note what is not here: an invitation token, or anything it could be derived from. account.Invitation has no such field, so a template cannot print one even by accident — an invitation link on this page would let any reader join as the invitee.

func TeamSwitcher

func TeamSwitcher(teams []TeamChoice) templ.Component

TeamSwitcher moves the session between the teams a person belongs to.

Every entry is a submit button in one POST form. A link would be a GET that changes what the whole dashboard is scoped by, and prefetchers and crawlers follow links — being moved into another team by a page you merely hovered over is a confusing way to end up acting somewhere you did not choose.

The list is only the chrome's copy of what the session already proved. Membership is checked again on the way in, so an entry forged into this form switches into nothing.

details/summary rather than a scripted menu: the dashboard's one piece of state that must work is the control that says which team you are in, and it should not need JavaScript to open.

The bordered box is not decoration. Sitting flush in a column of navigation links, this read as one more link — and the one control on the page that changes what every query is scoped by should not look like a way to go to a page. The border is what says it opens something.

Shares a name with the user menu so the browser closes one when the other opens. Two panels hanging over the sidebar at once is the kind of thing a scripted menu needs a listener for; native exclusive details does it for free.

func TemplateList

func TemplateList(d TemplateListData) templ.Component

TemplateList offers the stacks that can be deployed in one action.

func UserMenu

func UserMenu(owner identity.Owner, canSignOut bool) templ.Component

UserMenu is the sidebar's floor: who the session belongs to, and the actions that belong to the session rather than to any page.

A details/summary for the same reason the switcher is one — the control that signs you out is the last thing that should need JavaScript to open — and it opens upward because it sits on the floor of the panel.

canSignOut is false on a token-authenticated install. There is no session to end there and /sign-out is not routed at all, so the buttons would 404; drawing nothing is better than drawing a control that cannot work. It is the same question the switcher answers by having no teams to offer.

func Volumes

func Volumes(d VolumesData) templ.Component

Types

type AccountData

type AccountData struct {
	Email string
	Name  string

	// Team and Role are the ones the session is currently acting as, so that
	// somebody who belongs to several knows which of them this page was reached
	// through before they sign out of everything.
	Team string
	Role string

	// Since is when this session began, which is the only thing the sessions
	// section has to say about it that a person would recognise.
	Since string

	// HasPassword decides whether the password section offers to set a first one
	// or to replace one. It is the viewer's own fact about their own account,
	// read from their own session — there is no route here that answers it about
	// anybody else.
	HasPassword bool

	// RecentAuth reports that this session proved who holds it recently enough
	// to change how the account is signed in to.
	//
	// It is what makes a forgotten password recoverable without a second kind of
	// mailed token: a session started from a link in the person's own mail
	// carries exactly the authority a reset link would have carried, and it
	// carries it for ten minutes rather than for the life of the cookie.
	RecentAuth bool

	// MinLength mirrors the service's own floor onto the input, so the browser
	// says it before a round trip does. The rule is still enforced there.
	MinLength int

	Error  string
	Notice string
}

AccountData is what a person can see and change about themselves, as opposed to about the install or about their team.

Everything on it is read from the viewer's own resolved session. There is no field here that could be filled in about anybody else, and no route on this page takes an identifier from the request — which is what keeps "does this address have a password" a question the server never answers.

type Accounts

type Accounts interface {
	// IssueMagicLink mints a sign-in link, reporting whether the address
	// belongs to anyone. Nothing is mailed when it does not, and nothing about
	// the difference reaches the visitor.
	IssueMagicLink(
		ctx context.Context, email string, ttl time.Duration,
	) (raw string, user account.User, existed bool, err error)

	// EnsureUser creates a person, or returns the one already there. Used only
	// to bootstrap the configured owner of a fresh install.
	EnsureUser(ctx context.Context, email, displayName string) (account.User, error)

	// ConsumeMagicLink spends a link and returns the person it proves. Unknown,
	// expired and already-spent links all come back as account.ErrTokenInvalid.
	ConsumeMagicLink(ctx context.Context, raw string) (account.User, error)

	// AuthenticatePassword verifies an address and a password. Every failure —
	// unknown address, no password set, wrong password — is the single
	// account.ErrCredentialInvalid, so no caller can tell them apart or report
	// a difference that is not there to report.
	AuthenticatePassword(ctx context.Context, email, password string) (account.User, error)

	// HasPassword reports whether the viewer's own account has one, which is
	// what decides between offering to add and offering to change. By user id,
	// never by address: the same question asked about an address is the
	// disclosure the sign-in page is built to withhold.
	HasPassword(ctx context.Context, userID uuid.UUID) (bool, error)

	// SetPassword adds or replaces one. It takes a session id rather than a
	// user id so that whose password it is comes from the session row, and it
	// checks recent authentication itself rather than trusting this package to
	// have done it.
	SetPassword(
		ctx context.Context, sessionID uuid.UUID, password string, proof account.Proof,
	) error

	// RemovePassword takes one off, leaving the emailed link. Allowed because
	// the link never stopped working, so this cannot lock anybody out — and
	// refusing would mean a password added once can never be taken back.
	RemovePassword(ctx context.Context, sessionID uuid.UUID, proof account.Proof) error

	// TeamsFor lists the teams a person may act as.
	TeamsFor(ctx context.Context, userID uuid.UUID) ([]account.Membership, error)

	// User returns the person a session resolved to, so the account page can
	// name them. By id and never by address: a lookup by address reachable from
	// a handler would answer whether somebody has an account.
	User(ctx context.Context, userID uuid.UUID) (account.User, error)

	// BootstrapOwner gives someone with no team one to act as, handing the
	// first of them the team the install was already running as.
	BootstrapOwner(ctx context.Context, teamID, teamName string, user account.User) error

	// CreateSession opens a session onto a team, returning the token for the
	// cookie. Membership is verified there, not trusted from here.
	CreateSession(
		ctx context.Context, userID uuid.UUID, teamID, userAgent, ip string, ttl time.Duration,
	) (string, error)

	// ResolveSession returns the session a token stands for. It is how sign-out
	// everywhere learns whose sessions to end: from the cookie, never from
	// anything the request says about itself.
	ResolveSession(ctx context.Context, raw string) (account.Session, error)

	// SwitchTeam points a session at another team. Membership is verified there,
	// against the session's own user rather than against anything the request
	// supplied, which is what makes a crafted team id switch into nothing.
	SwitchTeam(ctx context.Context, sessionID uuid.UUID, teamID string) error

	// RevokeSession ends one session — sign-out on this browser.
	RevokeSession(ctx context.Context, raw string) error

	// RevokeAllSessions ends every session a person holds, which is the only
	// answer to a cookie that has been copied off a machine.
	RevokeAllSessions(ctx context.Context, userID uuid.UUID) error

	// InvitationFor reads an invitation without spending it, so a visitor who
	// is not signed in can be sent a link to the address it names instead of
	// being let in on the strength of holding the token.
	InvitationFor(ctx context.Context, raw string) (account.Invitation, error)

	// AcceptInvitation spends one. It checks the address itself, so a
	// signed-in stranger holding the token is refused there rather than here.
	AcceptInvitation(ctx context.Context, raw string, userID uuid.UUID) (string, account.Role, error)

	// ListMembers and ListPendingInvitations are what the team page shows.
	// Both are scoped by team, and the team comes from the caller's session
	// rather than from the request.
	ListMembers(ctx context.Context, teamID string) ([]account.Member, error)
	ListPendingInvitations(ctx context.Context, teamID string) ([]account.Invitation, error)

	// Invite issues an invitation and returns the token to mail. The token is
	// returned rather than stored anywhere readable, so the message is the only
	// place it exists.
	Invite(
		ctx context.Context, actor uuid.UUID, teamID, email string,
		role account.Role, ttl time.Duration,
	) (string, error)

	// RevokeInvitation withdraws one. The mail cannot be recalled, so deleting
	// the row is the only way to take an invitation back.
	RevokeInvitation(ctx context.Context, actor uuid.UUID, teamID string, id uuid.UUID) error

	// SetRole and RemoveMember change who may do what. Both check the actor's
	// own authority and refuse to leave the team without an owner.
	SetRole(ctx context.Context, actor uuid.UUID, teamID string, target uuid.UUID, role account.Role) error
	RemoveMember(ctx context.Context, actor uuid.UUID, teamID string, target uuid.UUID) error
}

Accounts is the sign-in surface's view of the account service.

An interface rather than the concrete service, for the same reason Apps is one: the sign-in page has to be testable without a database, and this package has no business knowing how a person is stored.

type ActivityData

type ActivityData struct {
	Items []app.Activity
}

type AddNodeData

type AddNodeData struct {
	// Settings describes what is stored. It never carries the token — see
	// cluster.Settings.
	Settings   cluster.Settings
	Configured bool

	// Command is the line to run. Empty until settings exist, because a
	// command with holes in it is worse than a prompt to fill them in.
	Command string
	Pool    string

	// Nodes and ClusterOK are the confirmation half.
	Nodes     []orchestrator.NodeInfo
	ClusterOK bool

	Error string
}

AddNodeData is the page that hands over a join command.

func (AddNodeData) IsNew

IsNew reports whether a node joined recently enough to be worth pointing at.

type AppDetailData

type AppDetailData struct {
	App         app.App
	Siblings    []app.App
	Tab         string
	Pods        []orchestrator.PodInfo
	Deployments []app.Deployment
	BaseURL     string

	// Net is the app's routing. Loaded alongside the rest rather than on its
	// own page: a panel nothing links to is a panel nobody finds, which is
	// exactly what happened the first time.
	Net app.Networking

	// ResolverName is which resolver answered the last domain check, so the
	// page can attribute what it reports rather than stating it as fact.
	ResolverName string

	// The resource tracks, with ceilings taken from the cluster's largest
	// node — what a single pod can actually be scheduled onto.
	CPULimit      ResourceSlider
	MemoryLimit   ResourceSlider
	CPURequest    ResourceSlider
	MemoryRequest ResourceSlider

	// RepoError is why the connected repository could not be read, shown on
	// the source card. A private repo, a typo and an outage all land here.
	RepoError string

	Notice string

	// Error is a refusal from the action that produced this render, shown with
	// the form that caused it rather than carried through a redirect.
	Error string

	// Deploy on push. HooksOn is whether the section is offered at all: a Git
	// app on an install with the webhook surface mounted.
	HooksOn bool
	Hook    app.Hook
	HookURL string

	// HookURLPublic says HookURL is on the configured public URL rather than
	// guessed from this request, which may be a tunnel nothing else can reach.
	HookURLPublic bool

	// HookSecret is set only on the response that issued it. It is never read
	// back, so this is the one render that can show it.
	HookSecret string
}

type Apps

type Apps interface {
	List(ctx context.Context, ownerID string) ([]app.App, error)
	Count(ctx context.Context, ownerID string) (int64, error)
	Get(ctx context.Context, ownerID, name string) (app.App, error)
	Create(ctx context.Context, ownerID string, in app.CreateInput) (app.App, error)
	Scale(ctx context.Context, ownerID, name string, replicas int32) (app.App, error)

	// Update changes what the app runs and how much it may use. Everything
	// here was decided at create and then frozen until this existed.
	Update(ctx context.Context, ownerID, name string, in app.UpdateInput) (app.App, error)

	// Branches lists a repository's branches, filtered by a query. Read with
	// ls-remote, so it costs one HTTPS request and no clone.
	Branches(ctx context.Context, repoURL, query string) ([]string, error)

	// Directories lists what is inside a repository at a path. GitHub only —
	// git cannot read a tree without fetching objects, so this asks the host.
	Directories(ctx context.Context, repoURL, path string) ([]string, error)
	Redeploy(ctx context.Context, ownerID, name string) error

	// Rollback puts an app back on one of its earlier releases, settings and
	// workload both.
	Rollback(ctx context.Context, ownerID, name string, releaseID uuid.UUID) error

	// CancelLiveDeployment stops whatever deploy an app is holding, and reports
	// app.ErrNotFound when it is holding none.
	CancelLiveDeployment(ctx context.Context, ownerID, name string) error
	Delete(ctx context.Context, ownerID, name string) error
	Deployments(ctx context.Context, ownerID string, appID uuid.UUID, limit int32) ([]app.Deployment, error)

	// DeployActivity is deploys per day for the overview chart, with every day
	// in the window present whether or not it had any.
	DeployActivity(ctx context.Context, ownerID string, days int) (app.DeployActivity, error)

	// Storage. Attaching holds the app to one replica and makes its deploys
	// recreate rather than roll; the service refuses what it cannot honour.
	AttachVolume(ctx context.Context, ownerID, appName string, in app.VolumeInput) (app.Volume, error)
	ResizeVolume(ctx context.Context, ownerID, appName, volumeName string, sizeBytes int64) error
	DeleteVolume(ctx context.Context, ownerID, appName, volumeName string, detach bool) error

	// Environment. A secret's value never comes back out — SetVariable is how
	// it is replaced, and there is no read path for it by design.
	SetVariable(ctx context.Context, ownerID, appName string, in app.VariableInput) error
	DeleteVariable(ctx context.Context, ownerID, appName, key string) error

	// SetHealth points the readiness probe at a path, and optionally lets the
	// same path restart the container.
	SetHealth(ctx context.Context, ownerID, name, healthPath string, liveness bool) error

	// Links are the dependencies between apps, recorded when a variable naming
	// another app is written — the only moment a sealed value is readable.
	Links(ctx context.Context, ownerID string) ([]app.Link, error)
	RecentActivity(ctx context.Context, ownerID string, limit int32) ([]app.Activity, error)

	// Projects are the canvases apps are drawn on. Project and Projects both
	// create the default one when a team has none, so no caller has to decide
	// whether a team has been set up yet.
	Projects(ctx context.Context, ownerID string) ([]app.Project, error)
	Project(ctx context.Context, ownerID, slug string) (app.Project, error)
	ProjectByID(ctx context.Context, ownerID string, id uuid.UUID) (app.Project, error)
	CreateProject(ctx context.Context, ownerID, name string) (app.Project, error)
	ListInProject(ctx context.Context, ownerID string, projectID uuid.UUID) ([]app.App, error)

	// SetPosition and ClearPositions are the canvas arrangement, which belongs
	// to the team rather than to whoever last dragged something.
	SetPosition(ctx context.Context, ownerID, name string, x, y int32) error
	ClearPositions(ctx context.Context, ownerID string, projectID uuid.UUID) error
}

Apps is the workload lifecycle this server drives.

An interface rather than the concrete service so the dashboard can be tested without a database. It is defined here, at the consumer, which keeps the app package free of knowledge about who calls it.

type BranchData

type BranchData struct {
	Branches []string

	// Error is why the remote could not be read, already reduced to something
	// short enough to sit under a field. A private repository, a typo and an
	// outage all land here and none of them is worth a red field.
	Error string

	// Searched distinguishes "nothing matched what you typed" from "we have
	// not looked yet", which render as different things and would otherwise
	// both be an empty list.
	Searched bool
}

BranchData is what the branch picker swaps in.

type CanvasData

type CanvasData struct {
	Nodes  []CanvasNode
	Edges  []CanvasEdge
	Width  int
	Height int

	// Empty distinguishes "no apps" from "apps that do not refer to each
	// other" — the second is a normal install, the first needs a prompt.
	Empty bool

	// Project is the canvas being drawn, and Projects is every one the team
	// has, so the switcher does not need a second request.
	Project  app.Project
	Projects []app.Project

	// Open is the app whose panel is showing, nil when none is. It is the same
	// data the app page used to render, because the panel IS the app page now.
	Open *AppDetailData
}

CanvasData is the whole graph.

type CanvasEdge

type CanvasEdge struct {
	// Path is an SVG path: out of the bottom of the dependent, up into the
	// bottom of the dependency. Orthogonal rather than curved, because a
	// straight line through three other cards is harder to follow than a
	// corner.
	Path string
	Via  string

	// From and To name the cards the edge joins, so the script can find the
	// paths that need re-routing when one of them moves.
	From string
	To   string
}

CanvasEdge is one app's dependency on another.

type CanvasNode

type CanvasNode struct {
	App    app.App
	X, Y   int
	Height int

	// Pinned means somebody dragged this card, so its position is stored
	// rather than computed.
	Pinned bool

	// Volume is the storage drawn attached beneath the card. Only the first is
	// shown: a second would need the card to grow, and no app in this engine
	// has one yet.
	Volume *app.Volume
}

CanvasNode is one app as the graph draws it.

type ClusterData

type ClusterData struct {
	Tab     string
	OK      bool
	Error   string
	Summary orchestrator.ClusterSummary
	Nodes   []orchestrator.NodeInfo
	Pods    []orchestrator.PodInfo

	// CanAddNode and CanManageNodes are separate because the abilities are.
	// Joining needs a key to seal a token; draining needs a credential that can
	// evict across namespaces, and an install can have either without the
	// other. Each gates only what it actually governs, so a link never leads
	// somewhere its viewer would be refused.
	CanAddNode     bool
	CanManageNodes bool
}

type Crumb

type Crumb struct {
	Label string
	Href  string
}

Crumb is one segment of the breadcrumb in the top bar. An empty Href renders as the current, non-navigable segment.

type DNSRecord

type DNSRecord struct {
	Type  string
	Name  string
	Value string
}

DNSRecord is a record somebody has to create at their provider.

Three fields because that is what every provider's form asks for. It was two paragraphs of prose before, which is the wrong shape for a value that gets retyped into another system.

type DefaultSlots

type DefaultSlots struct{}

DefaultSlots is the engine's own chrome: the navigation a single-owner self-hosted install needs, and nothing else.

func (DefaultSlots) Slots

func (DefaultSlots) Slots(ctx context.Context, r *http.Request) Slots

type DeployLogsData

type DeployLogsData struct {
	App    string
	Deploy app.DeployLogs

	// Build is what produced this deployment's image, when anything did.
	Build    app.Build
	HasBuild bool

	// HTTP is what the ingress controller recorded for this app.
	HTTP app.HTTPLogs

	// HTTPBuckets is the whole timeline, computed before paging.
	HTTPBuckets []app.HTTPBucket

	// Find is the search inside a log's own text, and Method narrows the
	// request list to one verb. Separate from the page's query only in name:
	// they travel the same way and are read the same way.
	Find   string
	Method string

	// Methods is what this app's requests actually used, so the selector
	// offers nothing that could only return an empty list.
	Methods []string

	// HTTPPage is the window of requests being shown. The chart above them is
	// deliberately not paged: it summarises the traffic, and a summary of one
	// page of it would answer a question nobody asked.
	HTTPPage Page

	// View is which of the three logs is showing.
	View     string
	Previous bool
	Error    string

	// DeployID is this deployment, so the build pane can address itself for
	// the poll that follows a running build.
	DeployID string

	// Pane asks for one pane rather than the whole sheet.
	Pane bool
}

DeployLogsData is the sheet opened from one deployment.

type DirectoryData

type DirectoryData struct {
	// Path is where the listing is standing, so an entry can be offered as the
	// full path a build would use rather than a bare name.
	Path string

	// Parent is one level up, or empty at the root.
	Parent string

	Directories []string

	// Error is why the tree could not be read, already short enough to sit
	// under a field. A non-GitHub host lands here too — that is a limit worth
	// stating rather than a field that quietly stops helping.
	Error string

	Searched bool
}

DirectoryData is what the directory picker swaps in.

type EventsData

type EventsData struct {
	OK     bool
	Error  string
	Events []orchestrator.EventInfo

	// Page is the window being shown, and Read is how much of the stream was
	// looked at to build it — shown because a search that found nothing needs
	// to say how far it looked.
	Page Page
	Read int
}

type ExtraRoutes added in v0.2.0

type ExtraRoutes struct {
	// Member is behind identity and the member gate: reading, and every
	// change a redeploy undoes.
	Member func(r chi.Router)
	// Admin is behind the admin gate: what a redeploy does not undo.
	Admin func(r chi.Router)
	// Owner is behind the owner gate: who can administer, and what it costs.
	Owner func(r chi.Router)
}

ExtraRoutes are what a wrapping application mounts inside the engine's role gates. Each is called with the group's router once, while the handler is being built, and may register any routes it likes there.

type Flash

type Flash struct {
	Kind FlashKind
	Text string
}

Flash is a message waiting to be shown once.

func (Flash) Assertive

func (f Flash) Assertive() bool

Assertive reports whether a screen reader should interrupt for this.

Only failures do. A polite live region is announced when the user pauses, which is right for "scaled to 3 replicas" and wrong for the one message that says the thing they asked for did not happen.

func (Flash) Class

func (f Flash) Class() string

Class maps a kind to its toast modifier. Unknown kinds draw as neutral rather than as nothing: a message with a mangled kind is still a message.

type FlashKind

type FlashKind string

FlashKind decides how a message is drawn, and nothing else.

const (
	FlashOK   FlashKind = "ok"
	FlashErr  FlashKind = "err"
	FlashWarn FlashKind = "warn"
)

type Hooks added in v0.2.0

type Hooks interface {
	GitHook(ctx context.Context, ownerID, name string) (app.Hook, error)
	EnableGitHook(ctx context.Context, ownerID, name string) (string, error)
	DisableGitHook(ctx context.Context, ownerID, name string) error
	DeliverPush(ctx context.Context, appID uuid.UUID, p app.Push) (string, error)
}

Hooks is deploy on push: a webhook per app that a Git host calls.

type Joiner

type Joiner interface {
	Settings(ctx context.Context) (cluster.Settings, error)

	// DNS is how the install publishes hostnames. It sits here rather than in
	// its own seam because both are one thing: install-wide cluster settings an
	// owner sets once, stored together and gated together.
	DNS(ctx context.Context) (cluster.DNS, error)
	SetDNS(ctx context.Context, target, prefix string) error
	SetJoin(ctx context.Context, serverURL, token string, by uuid.UUID) error
	Command(ctx context.Context, pool string) (string, error)
}

Joiner is the dashboard's view of how a machine joins this cluster.

An interface rather than the concrete type, for the reason Apps is one: the page has to be testable without a database, and this package has no business knowing how a join token is stored.

type Logger

type Logger interface {
	Logs(ctx context.Context, ownerID, appName string, req app.LogRequest) (app.Logs, error)

	// LogStream follows an app's container output until ctx is done. The
	// iterator holds a connection open while it is read, so the handler must
	// stop reading when the client goes away.
	LogStream(
		ctx context.Context, ownerID, appName string, req app.LogRequest,
	) (iter.Seq2[orchestrator.LogLine, error], error)
	DeploymentLogs(
		ctx context.Context, ownerID, appName string, deployID uuid.UUID, req app.LogRequest,
	) (app.DeployLogs, error)
	Deployment(
		ctx context.Context, ownerID, appName string, deployID uuid.UUID,
	) (app.Deployment, error)

	// BuildForDeployment is the build behind one deployment, when there was
	// one. ErrNotFound means the app was deployed from an image nobody built.
	BuildForDeployment(
		ctx context.Context, ownerID string, deployID uuid.UUID,
	) (app.Build, error)

	// DeploymentHTTPLogs is what the ingress controller recorded for this
	// app's hostnames.
	DeploymentHTTPLogs(ctx context.Context, ownerID, appName string) (app.HTTPLogs, error)
}

Logger is the dashboard's view of container output.

type LogsData

type LogsData struct {
	App      string
	Logs     app.Logs
	Error    string
	Previous bool
}

LogsData is the log pane.

type NavGroup struct {
	Heading string
	Items   []NavItem
}

NavGroup is a labelled block of navigation. An empty Heading renders the items without a header, which is how the primary group is expressed.

type NavItem struct {
	Label string
	Href  string
	// Icon names one of the inline glyphs in layout.templ. Unknown names
	// render nothing rather than a broken image.
	Icon   string
	Badge  string
	Active bool
}

NavItem is one entry in the sidebar.

type Nets

type Nets interface {
	Networking(ctx context.Context, ownerID, name string) (app.Networking, error)
	SetNetworking(ctx context.Context, ownerID, name string, httpsOnly, cnameOnly bool) error
	AddDomain(ctx context.Context, ownerID, name, host string) error
	VerifyDomain(ctx context.Context, ownerID, name string, id uuid.UUID) error
	RemoveDomain(ctx context.Context, ownerID, name string, id uuid.UUID) error

	// ResolverName says which resolver answers about custom domains, so the
	// page can attribute what it reports rather than stating it as fact.
	ResolverName() string

	// AllDomains is every custom domain on the install, unsettled first.
	AllDomains(ctx context.Context, ownerID string) ([]app.InstallDomain, error)

	// TargetStatus reports whether the configured CNAME target resolves,
	// as a sentence. Empty when there was nothing to check.
	TargetStatus(ctx context.Context, target string) string
}

Nets is the dashboard's view of an app's routing.

type NetworkingData

type NetworkingData struct {
	App string
	Net app.Networking

	// UntrustedCert means this app is served over https with a certificate
	// nobody will trust, because the install has no wildcard certificate.
	UntrustedCert bool

	// ResolverName is which resolver answered the last check.
	//
	// Shown because "this name does not resolve" is one resolver's opinion, and
	// somebody comparing it against their own dig output needs to know whether
	// the two asked the same server.
	ResolverName string

	// Settled reports that no domain here is still moving, which is what stops
	// the polling fragment asking again.
	Settled bool

	Notice string
	Error  string
}

NetworkingData is the app's routing panel.

type NewAppData

type NewAppData struct {
	Error string
	Form  NewAppForm

	// Source is what the person picked. Empty shows the picker instead of a
	// form: there is no sensible default, and choosing for them is how you end
	// up with a database deployed as a bare image.
	Source app.Source

	// Blueprint is the chosen source's settings, so the form can show what is
	// already decided rather than asking again.
	Blueprint app.Blueprint

	Sources []app.Blueprint

	// CanConfigureRegistry decides whether an unavailable Git source offers a
	// way out or only names who has one. The registry page is owner-only, so
	// linking a member to it would replace a dead end with a locked door.
	CanConfigureRegistry bool
}

type NewAppForm

type NewAppForm struct {
	Name     string
	Image    string
	Port     string
	Replicas string
	Env      string

	// The repository, for a source that is built rather than pulled.
	RepoURL    string
	RepoBranch string
	RepoSubdir string
}

NewAppForm preserves what the user typed so a validation error does not discard their input.

type NodeDetailData

type NodeDetailData struct {
	Node  orchestrator.NodeInfo
	Found bool

	// Pods are what remains on the node. Drained is true when nothing is left
	// that a drain would move, which is the condition for offering removal.
	Pods    []orchestrator.PodInfo
	Drained bool

	// CanManage is false when the orchestrator cannot take a node out of
	// service, so the page explains rather than offering buttons that fail.
	CanManage bool

	Error  string
	Notice string
}

NodeDetailData is one machine and what is still running on it.

type Options

type Options struct {
	Orchestrator orchestrator.Orchestrator
	Identity     identity.Provider
	Apps         Apps

	// Slots is optional; DefaultSlots is used when nil.
	Slots SlotProvider

	// Extra is routes a wrapping application mounts inside the engine's own
	// role gates, so its pages get the same session, identity and CSRF
	// handling as the engine's without re-implementing any of it.
	Extra ExtraRoutes

	// Authenticated reports whether a credential is required, purely so the
	// settings page can warn when it is not.
	Authenticated bool
	Version       string

	// AppDomain and WildcardTLS describe the platform's hostname policy. The
	// dashboard reads them to explain the install on the settings page; the
	// per-app scheme comes from app.App.TLS, which the app service populates.
	AppDomain   string
	WildcardTLS bool

	// Joiner, when set, puts the add-node surface on the router. Left nil the
	// engine offers no way to add a node, which is right for an install with
	// no secret key: it could store nothing and hand out nothing.
	Joiner Joiner

	// Stacks, when set, puts the templates surface on the router.
	Stacks Stacks

	// Nets, when set, puts the per-app Networking surface on the router.
	Nets Nets

	// Hooks is deploy on push. Nil leaves the webhook endpoint unmounted and
	// the settings section off.
	Hooks Hooks

	// Logs, when set, puts the log surface on the router.
	Logs Logger

	// Registries, when set, puts the image registry surface on the router.
	// Nil leaves it off: an install with nowhere to push cannot build, and a
	// form for a setting nothing reads is a worse answer than no form.
	Registries Registries

	// Accounts, when set, puts the sign-in surface on the router. Left nil the
	// engine serves no sign-in page at all, which is right for an install
	// resolved by a shared token: a form that could never issue a session is a
	// worse answer than no form.
	Accounts Accounts

	// Mailer delivers sign-in links. Defaults to the logging transport, which
	// is the break-glass path when mail breaks after accounts are switched on.
	Mailer notify.Mailer

	// BaseURL is the public URL links are built from. Required with Accounts,
	// and deliberately not derived from the request: a link built from the Host
	// header is one an attacker can point at their own server and have Yacht
	// mail to a real person.
	BaseURL string

	// MagicLinkTTL is how long a sign-in link stays redeemable. Defaults to
	// defaultMagicLinkTTL.
	MagicLinkTTL time.Duration

	// SessionTTL is how long a signed-in browser stays signed in. Defaults to
	// defaultSessionTTL.
	SessionTTL time.Duration

	// MailTransport names the delivery route — "smtp", "resend" or "log" — so
	// the settings page can show whether links are actually being sent.
	MailTransport string

	// BootstrapEmail is the address allowed to create itself on first sign-in.
	//
	// A link is only ever issued to an address that already has an account,
	// which is what stops the sign-in form filling the user table. On a fresh
	// install that leaves nobody able to get in at all: no user, so no link, so
	// no user. This names the one address exempt from that, and only while it
	// has no account — once it does, it is an ordinary person like anyone else.
	BootstrapEmail string

	// BootstrapTeamID and BootstrapTeamName are the install's configured owner
	// — YACHT_OWNER_ID and YACHT_OWNER_NAME. The first person to sign in
	// inherits that team, so the apps an install already deployed under it stay
	// reachable once accounts are switched on. Required with Accounts.
	BootstrapTeamID   string
	BootstrapTeamName string

	Logger *slog.Logger
}

Options configures the dashboard server.

All three seams arrive here as dependencies. Nothing in this package constructs an orchestrator, resolves an identity, or decides what the chrome looks like — which is what makes the whole surface reusable by an application the engine has never heard of.

type OverviewData

type OverviewData struct {
	OwnerName    string
	ClusterOK    bool
	ClusterError string
	AppCount     int64
	Summary      orchestrator.ClusterSummary
	Apps         []app.App

	// Activity is deploys per day. Zero days means the chart is not shown at
	// all, which is the state an install has before it can read its own
	// history — not the same as a window that genuinely had no deploys.
	Activity app.DeployActivity
}

type Page

type Page struct {
	// Query is the search somebody typed, echoed back so the box keeps it.
	Query string

	// Number is the 1-based page being shown, and Total is how many there are.
	Number int
	Total  int

	// Matched is how many rows the search found, before paging.
	Matched int

	// From and To are the 1-based bounds of this page, for "51–100 of 412".
	From int
	To   int

	// Base is the path the controls link to, without a query string.
	Base string

	// Extra carries any other query parameters the surface needs kept across
	// paging — a log view, a pod name — so a page link does not silently
	// change what is being looked at.
	Extra url.Values
}

Page is one window onto a list, and what a template needs to draw the controls around it.

Computed from the whole result set rather than from a database cursor, because both surfaces using this read from Kubernetes, which returns what it returns. Slicing here is honest about that: it makes the page small, and it does not pretend the read was.

func (Page) Empty

func (p Page) Empty() bool

Empty reports whether there is nothing to show at all.

func (Page) Filtered

func (p Page) Filtered() bool

Filtered reports whether a search narrowed the list, which is the difference between "nothing here" and "nothing matched".

func (Page) HasNext

func (p Page) HasNext() bool

func (Page) HasPrev

func (p Page) HasPrev() bool

HasPrev and HasNext gate the controls, so a page never offers a link to somewhere that does not exist.

func (Page) Href

func (p Page) Href(number int) string

Href builds the link to another page, keeping the search and anything else the surface asked to carry.

func (Page) NextHref

func (p Page) NextHref() string

func (Page) PrevHref

func (p Page) PrevHref() string

type PlatformDNSData

type PlatformDNSData struct {
	DNS   cluster.DNS
	Error string

	// Domains is every custom domain on the install, unsettled first. This is
	// where the background checker's work becomes visible: one app's page shows
	// one app's domains, and nothing answered "which are stuck".
	Domains []app.InstallDomain

	// TargetResolves reports whether the configured CNAME target itself
	// resolves. Empty means nothing was checked — no target, or no resolver.
	TargetResolves string

	ResolverName string
}

PlatformDNSData is the install's DNS settings, and every domain that depends on them.

type ProjectListData

type ProjectListData struct {
	Projects []app.Project
	Error    string
}

ProjectListData is the page listing a team's projects.

type Registries

type Registries interface {
	Settings(ctx context.Context) (registry.Settings, error)
	Set(ctx context.Context, by uuid.UUID, host, repository, username, password string, insecure bool) error
	Clear(ctx context.Context) error

	// CanStore reports whether a password can be sealed. Asked here rather
	// than handing the web layer an encryption key it has no other use for.
	CanStore() bool
}

Registries is the dashboard's view of where images go.

type RegistryData

type RegistryData struct {
	Registry registry.Settings

	// HasKey reports whether a password can be stored at all. The page says so
	// before showing a form rather than after a submission fails.
	HasKey bool

	Notice string
	Error  string
}

RegistryData is the install's registry page.

type ResourceSlider

type ResourceSlider struct {
	// Name is the form field, in the unit the slider works in — millicores or
	// mebibytes — rather than in Kubernetes' string grammar. The handler turns
	// the number back into a string, so nothing here has to parse one.
	Name string

	Label string

	// Value and Max are in the slider's own unit.
	Value int64
	Max   int64
	Step  int64

	// Display and MaxDisplay are the same numbers as people say them.
	Display    string
	MaxDisplay string

	// Unset means no limit is configured, which is a different answer from a
	// small one and is drawn differently.
	Unset bool
}

ResourceSlider is one track: what it is set to now, and how far it can go.

type Server

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

Server serves the dashboard.

func New

func New(opts Options) (*Server, error)

New validates options and returns a server.

func (*Server) FlashErr added in v0.2.0

func (s *Server) FlashErr(w http.ResponseWriter, r *http.Request, text string)

func (*Server) FlashOK added in v0.2.0

func (s *Server) FlashOK(w http.ResponseWriter, r *http.Request, text string)

FlashOK, FlashErr and FlashWarn leave a message for the next full page render, across a redirect. Same mechanism as the engine's own actions use.

func (*Server) FlashWarn added in v0.2.0

func (s *Server) FlashWarn(w http.ResponseWriter, r *http.Request, text string)

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the HTTP routes.

Authenticated routes live in a group behind identity.Middleware rather than each checking for themselves. A per-handler check is one a handler can forget, and the forgotten one is the security bug.

func (*Server) Render added in v0.2.0

func (s *Server) Render(w http.ResponseWriter, r *http.Request, page templ.Component)

Render draws a page inside the engine's layout, with the chrome the SlotProvider supplies and whatever flash the last request left. For a wrapping application's own pages, so they look like the engine's.

func (*Server) RenderStatus added in v0.2.0

func (s *Server) RenderStatus(w http.ResponseWriter, r *http.Request, status int, page templ.Component)

RenderStatus is Render with a status other than 200 — a form re-shown with its refusal, say.

type SettingsData

type SettingsData struct {
	OwnerID       string
	OwnerName     string
	Authenticated bool
	Version       string
	ClusterOK     bool
	AppDomain     string
	WildcardTLS   bool

	// AccountsOn, MailTransport and SessionTTL describe the sign-in posture.
	// Shown because otherwise the only way to find out whether accounts are on
	// — and whether links are actually being delivered — is to sign out and
	// see what happens.
	AccountsOn    bool
	MailTransport string
	SessionTTL    string
}

type SignInData

type SignInData struct {
	Email string
	Error string

	// Method names which of the two forms was submitted, so that the address is
	// put back in the one it was typed in and the error is drawn beside it.
	//
	// Derived only from the route that was posted to — a fact the submitter
	// chose — so it says nothing whatever about the account.
	Method SignInMethod
}

SignInData is the state of the sign-in form.

Error is what the visitor did wrong or what the server refused, never anything about whether the address is registered or whether it has a password — those are the two facts this page exists to keep quiet about.

There is deliberately no Password field. A submitted password rendered back into the HTML would sit in a page somebody may screenshot, print, or paste into a bug report, and no-store reaches none of those.

type SignInMethod

type SignInMethod string

SignInMethod is which form a submission came from.

const (
	SignInByLink     SignInMethod = "link"
	SignInByPassword SignInMethod = "password"
)

type SlotProvider

type SlotProvider interface {
	Slots(ctx context.Context, r *http.Request) Slots
}

SlotProvider builds the chrome for a request.

Taking the request, rather than a fixed Slots value, is what allows the chrome to vary per principal — which is precisely what a multi-tenant wrapper needs and a single-owner engine does not.

type SlotProviderFunc

type SlotProviderFunc func(ctx context.Context, r *http.Request) Slots

SlotProviderFunc adapts a function to SlotProvider.

func (SlotProviderFunc) Slots

func (f SlotProviderFunc) Slots(ctx context.Context, r *http.Request) Slots

type Slots

type Slots struct {
	// Title is the document title.
	Title string

	// Breadcrumb is the trail in the top bar. A wrapping application prepends
	// its own segments — an account or environment, say — without the engine
	// needing a concept for them.
	Breadcrumb []Crumb

	// BrandName and BrandHref label the top-left corner. A wrapping
	// application overrides these to present its own product.
	BrandName string
	BrandHref string

	// Nav is the sidebar navigation. A wrapping application may append its
	// own groups, or replace the set entirely.
	Nav []NavGroup

	// HeaderTools renders at the right of the top bar. This is where a
	// commercial layer would put a balance indicator or usage meter.
	HeaderTools templ.Component

	// SidebarTop renders above the navigation — the natural home for an
	// organisation or project switcher.
	SidebarTop templ.Component

	// SidebarFooter renders at the bottom of the sidebar.
	SidebarFooter templ.Component

	// FullBleed drops the centred, padded content wrapper so the page fills
	// the window and manages its own scrolling.
	//
	// A chrome decision rather than a page one, which is why it lives here: a
	// wrapping application that replaces the layout decides for itself which
	// of its pages want the whole window, and the engine's own answer is not
	// binding on it.
	FullBleed bool

	// Banner renders above the page content, full width. Intended for
	// account-level notices such as a low balance or a pending suspension.
	Banner templ.Component

	// Bare drops the sidebar and the top bar, leaving the page the whole
	// window.
	//
	// For the pages somebody meets before they are let in. Emptying the
	// individual slots is not the same thing: the navigation goes, but the
	// sidebar column and the top bar stay, so a sign-in form sits beside an
	// empty rail and under an empty header — chrome for an application the
	// visitor has no access to yet.
	//
	// A chrome decision rather than a page one, which is why it lives here
	// beside FullBleed. A wrapping application that replaces the layout decides
	// for itself what its signed-out pages look like.
	Bare bool

	// Flash is the outcome of whatever the last request did, shown once.
	//
	// Filled by the server immediately before rendering rather than by a
	// SlotProvider: reading it also clears it, and a provider is handed the
	// request without the response it would need to do that. A wrapping
	// application therefore inherits toasts without implementing anything.
	Flash *Flash
}

Slots is the chrome around a page.

Every field here is an extension point. Adding a field is safe; changing the meaning of one is a breaking change for anything wrapping the engine, so treat this struct as public API.

type Stacks

type Stacks interface {
	DeployTemplate(ctx context.Context, ownerID, slug, stack string) (app.Project, error)
}

Stacks is the dashboard's view of deploying a template.

Separate from Apps because it is a different verb: Apps creates one workload, this creates several and the project holding them. Keeping it apart means the templates page cannot reach anything else on the app service by accident.

type Step

type Step struct {
	Label string

	// Detail is what the last check actually observed.
	//
	// The reason this type exists rather than a status word. "Points elsewhere"
	// is not actionable; "points at ghs.googlehosted.com" is, and the difference
	// is the whole of the user's complaint about waiting without feedback.
	Detail string

	// Commands are what to run about it, each shown on its own line with a
	// copy button. Kept out of Detail because a command inside a sentence is
	// retyped by hand, and a command retyped by hand is the one with the typo.
	Commands []string

	// At is when this stage last changed. Zero renders nothing rather than the
	// epoch.
	At time.Time

	State StepState
}

Step is one stage of something that arrives over time.

type StepState

type StepState string

StepState is how far a stage has got. The values are the CSS class names directly, declared in assets/css/input.css under @layer components — a name assembled in Go is invisible to Tailwind's scanner, so anything returned from here has to exist in the stylesheet already.

const (
	// StepWait is a stage nothing has reached. Drawn, but making no claim.
	StepWait StepState = "step-wait"
	// StepActive is the stage being worked on now.
	StepActive StepState = "step-active"
	// StepDone is settled and behind us.
	StepDone StepState = "step-done"
	// StepErr is settled and wrong, which is different from still waiting.
	StepErr StepState = "step-err"
)

type Surfaces

type Surfaces struct {
	DNS      bool
	Registry bool
}

Surfaces is which optional pages this install actually has.

The sidebar is built from a path and a context, with no access to the server's dependencies, so without this it offers every entry regardless of whether the route behind it was mounted. A link to a 404 is a worse answer than a missing link: it says the feature exists and is broken.

This is the same failure the Networking panel had, and the DNS page had it silently from the day it was written — a page with no way in.

func SurfacesFromContext

func SurfacesFromContext(ctx context.Context) Surfaces

SurfacesFromContext returns the optional pages available to this request.

type TeamChoice

type TeamChoice struct {
	ID   string
	Name string
	Role account.Role

	// Active marks the team the session is acting as — the owner every query on
	// this request is scoped by.
	Active bool
}

TeamChoice is one team a person may act as, as the chrome needs it.

Public because the switcher is chrome, and chrome is the seam a wrapping application replaces: its own SlotProvider reads the same list out of the request and renders it however its product does.

func TeamsFromContext

func TeamsFromContext(ctx context.Context) []TeamChoice

TeamsFromContext returns the teams the request's own session may act as.

Empty on an install with no account service, and for a request with no session: in both cases there is nothing to switch between.

type TeamPageData

type TeamPageData struct {
	TeamID   string
	TeamName string

	// Viewer is the role of the person looking at the page. The page offers
	// only the controls their role can actually use — an admin is not shown a
	// role selector they would be refused for using.
	Viewer   account.Role
	ViewerID uuid.UUID

	Members     []account.Member
	Invitations []account.Invitation

	// Error is a message from the action that redirected here, if it failed.
	Error string
}

TeamPageData is the team management page.

type TemplateListData

type TemplateListData struct {
	Templates []app.Template
	Stack     string
	Slug      string
	Error     string
}

TemplateListData is the page offering the stacks that can be deployed.

type VolumesData

type VolumesData struct {
	OK      bool
	Error   string
	Volumes []orchestrator.VolumeInfo
}

Directories

Path Synopsis
ui
icon
templui component icon - version: v1.12.1 installed by templui v1.12.1 📚 Documentation: https://templui.io/docs/components/icon
templui component icon - version: v1.12.1 installed by templui v1.12.1 📚 Documentation: https://templui.io/docs/components/icon
utils
templui util templui.go - version: v1.12.1 installed by templui v1.12.1
templui util templui.go - version: v1.12.1 installed by templui v1.12.1

Jump to

Keyboard shortcuts

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