domain

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: May 10, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type APIKeyItem

type APIKeyItem struct {
	KeyMetadata
	// ShortName is the Kubernetes Secret name (without namespace prefix).
	ShortName string
}

APIKeyItem holds the displayable metadata for a single API key (no token).

type APIKeyResult

type APIKeyResult struct {
	RawToken string // shown once only
	KeyMetadata
}

APIKeyResult is the result of a successful Create operation. RawToken is the opaque token and is returned only on creation.

type AppError

type AppError struct {
	Code    ErrorCode
	BizCode BusinessErrorCode // optional; when non-empty, serialised into the errorCode response field
	Message string
	Cause   error // not exposed to callers; used for logging
	Detail  any   // structured extra info (e.g. PoolStatusDetail), serialized in response
}

AppError is a domain-level error that carries an HTTP-status-mapped code, a user-visible message, an optional wrapped cause for logging, and optional structured detail for the API response.

func NewAPIKeyRequired

func NewAPIKeyRequired(msg string) *AppError

NewAPIKeyRequired constructs a 422 AppError carrying BizErrAPIKeyRequired. Use this when the current user has no API Key and must create one before the requested operation can proceed.

func NewBadRequest

func NewBadRequest(msg string) *AppError

NewBadRequest constructs a 400 AppError.

func NewConflict

func NewConflict(msg string) *AppError

NewConflict constructs a 409 AppError.

func NewForbidden

func NewForbidden(msg string) *AppError

NewForbidden constructs a 403 AppError.

func NewInternal

func NewInternal(msg string, cause error) *AppError

NewInternal constructs a 500 AppError with an underlying cause.

func NewNotFound

func NewNotFound(msg string) *AppError

NewNotFound constructs a 404 AppError.

func NewServiceUnavailable

func NewServiceUnavailable(msg string) *AppError

NewServiceUnavailable constructs a 503 AppError.

func NewTooManyRequests

func NewTooManyRequests(msg string, cause error, detail any) *AppError

NewTooManyRequests constructs a 429 AppError with an underlying cause.

func NewUnauthorized

func NewUnauthorized(msg string) *AppError

NewUnauthorized constructs a 401 AppError.

func (*AppError) Error

func (e *AppError) Error() string

func (*AppError) Unwrap

func (e *AppError) Unwrap() error

type AuthInfo

type AuthInfo struct {
	Namespace string
	Role      string
	User      string
	Team      string
	QuotaURL  string
	// AuthMethod indicates how the caller was authenticated: "apikey" or "jwt".
	AuthMethod string
	// Email is the user's email address (populated from JWT claims).
	Email string
	// Name is the user's display name (populated from JWT claims).
	Name string
}

AuthInfo holds the authenticated caller's identity extracted from the API key or JWT. It has no dependency on gin or net/http.

type AvailablePoolSummary

type AvailablePoolSummary struct {
	Name      string `json:"name"`
	Namespace string `json:"namespace,omitempty"`
	Idle      int32  `json:"idle"`
	Running   int32  `json:"running"`
	Starting  int32  `json:"starting"`
}

AvailablePoolSummary is a lightweight pool descriptor included in the discovery detail when a client references a missing pool. It intentionally avoids leaking full pool spec to keep the error response compact.

type AvailablePoolsDetail

type AvailablePoolsDetail struct {
	AvailablePools []AvailablePoolSummary `json:"availablePools"`
	Hint           string                 `json:"hint,omitempty"`
}

AvailablePoolsDetail is attached to 404 Not Found when CreateSandbox references a pool that does not exist. The caller can pick a pool from AvailablePools and retry without a round-trip to ListSandboxPools.

type AvailableQuotaURLSummary

type AvailableQuotaURLSummary struct {
	URL   string `json:"url"`
	Queue string `json:"queue,omitempty"`
	Free  string `json:"free,omitempty"`
}

AvailableQuotaURLSummary describes one quota that the caller may reference via spec.reservation.quotaURL.

type AvailableQuotaURLsDetail

type AvailableQuotaURLsDetail struct {
	AvailableQuotaURLs []AvailableQuotaURLSummary `json:"availableQuotaURLs"`
	Hint               string                     `json:"hint,omitempty"`
}

AvailableQuotaURLsDetail is attached to 400 Bad Request when a pool requests reservation but does not specify a quotaURL.

type AvailableTemplateSummary

type AvailableTemplateSummary struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	SyncSource  string `json:"syncSource,omitempty"`
}

AvailableTemplateSummary is a lightweight template descriptor included in the discovery detail when a client references a missing template.

type AvailableTemplatesDetail

type AvailableTemplatesDetail struct {
	AvailableTemplates []AvailableTemplateSummary `json:"availableTemplates"`
	Hint               string                     `json:"hint,omitempty"`
}

AvailableTemplatesDetail is attached to 404 Not Found when CreateSandboxPool references a template that does not exist.

type BusinessErrorCode

type BusinessErrorCode string

BusinessErrorCode is a machine-readable business error code carried alongside specific AppErrors that require special frontend handling (e.g. redirecting the user to another page instead of showing a generic toast). It is intentionally orthogonal to the HTTP status code: the same HTTP status can carry different BusinessErrorCodes depending on the scenario.

const (
	// BizErrAPIKeyRequired indicates the current user has no API Key and must
	// create one before performing this operation. The frontend should navigate
	// the user to the API Key management page rather than showing a generic error.
	BizErrAPIKeyRequired BusinessErrorCode = "API_KEY_REQUIRED"
)

type ClusterSummary

type ClusterSummary struct {
	ID    string `json:"id"`
	Name  string `json:"name,omitempty"`
	Local bool   `json:"local"`
}

ClusterSummary describes one cluster visible to the gateway's routing table. It is intentionally minimal: the full per-plane URLs and headers live in the private cluster config and should never be exposed through the public API.

type CreateAPIKeyInput

type CreateAPIKeyInput struct {
	Namespace   string
	User        string
	Team        string
	Description string
	ExpiresAt   time.Time // zero means no expiry
	// Import mode fields (admin-only).
	// When TokenHash is non-empty the key is imported using the given hash
	// (via CreateFromHash) instead of generating a new random token.
	TokenHash  string
	HashPrefix string
	IssuedAt   time.Time // preserve original issue time (import mode)
	QuotaURL   string
}

CreateAPIKeyInput carries parameters for issuing a new API key.

type CreateSandboxInput

type CreateSandboxInput struct {
	ClusterID       string // target cluster ID parsed from pool name prefix; empty means local
	PoolName        string
	Namespace       string
	Image           string
	ContainerImages map[string]string
	Labels          map[string]string
	Annotations     map[string]string
	Metadata        map[string]string
	StartupTimeout  time.Duration // 0 means no timeout
	IdleTimeout     time.Duration // 0 means no expiry
	// PostStartHooks are actions to run after the sandbox transitions Starting → Running.
	// Serialized to a pod annotation at claim time; consumed by the controller.
	PostStartHooks []PostStartHookAction
}

CreateSandboxInput carries all parameters needed to create a new sandbox.

type CreateSandboxPoolInput

type CreateSandboxPoolInput struct {
	Name            string
	Namespace       string
	TemplateName    string // references a cluster-scoped SandboxTemplate (optional)
	Labels          map[string]string
	Annotations     map[string]string
	Spec            agentsv1alpha1.SandboxPoolSpec
	Team            string                 // from auth.Team, propagated to pod label
	User            string                 // from auth.User, propagated to pod label
	Overrides       *PoolTemplateOverrides // nil = no overrides
	ImagePullSecret *ImagePullSecretInput  // nil = no pull secret to materialise
}

CreateSandboxPoolInput carries all parameters needed to create a new SandboxPool.

type DeleteAPIKeyInput

type DeleteAPIKeyInput struct {
	// KeyID is the secret name (without namespace prefix).
	KeyID string
}

DeleteAPIKeyInput identifies which key to delete.

type DeleteSandboxPoolResult

type DeleteSandboxPoolResult struct {
	Name      string
	Namespace string
}

DeleteSandboxPoolResult is returned after a SandboxPool is deleted.

type DeleteSandboxResult

type DeleteSandboxResult struct {
	SandboxID string
	Namespace string
	PoolName  string
	PodName   string
	Status    string
}

DeleteSandboxResult is returned after a sandbox is released.

type EndpointReadiness

type EndpointReadiness struct {
	Ready   bool
	Message string
}

EndpointReadiness holds the readiness result for a single runtime endpoint.

type ErrorCode

type ErrorCode int

ErrorCode maps to HTTP status codes.

const (
	ErrCodeBadRequest          ErrorCode = 400
	ErrCodeUnauthorized        ErrorCode = 401
	ErrCodeForbidden           ErrorCode = 403
	ErrCodeNotFound            ErrorCode = 404
	ErrCodeConflict            ErrorCode = 409
	ErrCodeUnprocessableEntity ErrorCode = 422
	ErrCodeTooManyRequests     ErrorCode = 429
	ErrCodeServiceUnavailable  ErrorCode = 503
	ErrCodeInternal            ErrorCode = 500
)

type ExecCommandInput

type ExecCommandInput struct {
	Command        string
	TimeoutSeconds int // 0 means use default (30s)
}

ExecCommandInput carries the parameters for executing a one-shot command in a sandbox.

type ExecCommandResult

type ExecCommandResult struct {
	ExitCode int
	Stdout   string
	Stderr   string
}

ExecCommandResult holds the output of a one-shot command execution.

type ExecHookAction

type ExecHookAction struct {
	// Command is passed to sh -c inside the first container.
	Command string `json:"command"`
}

ExecHookAction runs a command inside the sandbox container.

type ExecTokenInfo

type ExecTokenInfo struct {
	SandboxID  string
	Namespace  string
	PodName    string
	Containers []string
}

ExecTokenInfo carries the information stored in an exec token. It is returned by ValidateExecToken after successful token validation.

type GetLogsOptions

type GetLogsOptions struct {
	// Container restricts log retrieval to the named container.
	// Empty means all containers.
	Container string
	// Lines limits the response to the last N log entries.
	// 0 means return all entries.
	Lines int
	// Source selects log origin:
	// "" or "stdout" → container stdout/stderr (default)
	// "<runtimeName>" → read logDir via exec
	Source string
}

GetLogsOptions controls log retrieval behaviour.

type HTTPPostHookAction

type HTTPPostHookAction struct {
	// Port is the target port of the in-sandbox service (required).
	Port int32 `json:"port"`
	// Path is the request path, e.g. "/init".
	Path string `json:"path"`
	// Body is an arbitrary JSON object serialized as the request body.
	Body map[string]any `json:"body,omitempty"`
	// Headers are extra HTTP headers to include (e.g. for authentication).
	Headers map[string]string `json:"headers,omitempty"`
}

HTTPPostHookAction sends a POST to a port/path inside the sandbox, routed through the gateway.

type ImagePullSecretInput

type ImagePullSecretInput struct {
	Registries []RegistryCredential
}

ImagePullSecretInput carries registry credentials attached by the caller at pool creation. Not persisted in domain form; the service layer immediately materialises it into a Kubernetes Secret (kubernetes.io/dockerconfigjson) with an OwnerReference to the pool.

type KeyMetadata

type KeyMetadata struct {
	// KeyID is the fully qualified secret identifier: "<namespace>/<name>".
	KeyID       string
	Namespace   string
	Role        string
	User        string
	Team        string
	QuotaURL    string
	Description string
	IssuedAt    time.Time
	ExpiresAt   time.Time // zero value means no expiry
	// SyncSource is "global" when the key was created/synced via ws-proxy.
	// Empty means locally-created or a legacy resource — both are treated as non-global.
	SyncSource string
	// RawToken is the full raw API key recovered from storage. Empty for legacy keys
	// that pre-date plaintext storage. Exposed via API to authorised callers for recovery.
	RawToken string
}

KeyMetadata holds the full metadata for an API key, mirroring what is stored in the backing Kubernetes Secret.

type ListAPIKeysResult

type ListAPIKeysResult struct {
	Items []APIKeyItem
}

ListAPIKeysResult is the result of a List operation.

type ListClustersResult

type ListClustersResult struct {
	Clusters []ClusterSummary `json:"clusters"`
}

ListClustersResult is returned by ClusterService.List and wraps the catalog so future fields (e.g. gateway health) can be added without breaking clients.

type ListSandboxesFilter

type ListSandboxesFilter struct {
	Namespace string
	PoolName  string
	Status    string // comma-separated multi-value supported (e.g. "Running,Failed")
	Team      string // when non-empty, only return sandboxes with this team label
	User      string // when non-empty, only return sandboxes with this user label
	Limit     int    // default 20, max 100; 0 means no limit
	Offset    int
}

ListSandboxesFilter holds optional filters for listing sandboxes.

type PodDiagnostic

type PodDiagnostic struct {
	// PodName is the name of the pod.
	PodName string
	// Phase is the agentbox pod phase (e.g. "starting", "failed").
	Phase string
	// Reason is a machine-readable cause, e.g. "ImagePullBackOff", "OOMKilled".
	Reason string
	// Message is a human-readable description.
	Message string
	// Events contains Warning events (only populated for Get, not List).
	Events []PodDiagnosticEvent
}

PodDiagnostic holds status detail for a single problematic pod. For List responses only Reason/Message (from Pod YAML) are populated. For Get responses Events (from K8s Events API) are also populated.

type PodDiagnosticEvent

type PodDiagnosticEvent struct {
	// Reason is the event reason, e.g. "Failed", "BackOff", "ErrImagePull".
	Reason string
	// Message is the human-readable event message.
	Message string
	// LastTimestamp is the RFC3339 time of the most recent occurrence.
	LastTimestamp string
	// Count is the total number of times this event fired.
	Count int32
}

PodDiagnosticEvent is a single Kubernetes Warning event for a pod.

type PoolStatusDetail

type PoolStatusDetail struct {
	Idle       int32  `json:"idle"`
	Running    int32  `json:"running"`
	Starting   int32  `json:"starting"`
	Stopping   int32  `json:"stopping"`
	Failed     int32  `json:"failed"`
	Hint       string `json:"hint,omitempty"`
	RetryAfter int    `json:"retryAfter,omitempty"` // seconds
}

PoolStatusDetail is attached to 409 Conflict when creating sandboxes and the pool has no idle pods available.

type PoolTemplateOverrides

type PoolTemplateOverrides struct {
	// Image overrides containers[0].Image; empty = no-op.
	Image string `json:"image,omitempty"`
	// ResourceMultiplier uniformly scales all container CPU and memory requests+limits,
	// and all reservation.replicaQuota values. Must be >= 1; 1 = no change.
	ResourceMultiplier int32 `json:"resourceMultiplier,omitempty"`
	// ImagePullSecretName is the deterministic Secret name injected into
	// spec.template.spec.imagePullSecrets; empty = no-op.
	ImagePullSecretName string `json:"imagePullSecretName,omitempty"`
}

PoolTemplateOverrides holds per-pool overrides applied on top of the referenced template. Applied in the service layer AFTER copying EmbeddedSandboxTemplate from the source template. The effective computed values are stored in spec, while the override intent is persisted in pool annotations so SyncTemplate can re-apply it against newer template versions.

type PostStartHookAction

type PostStartHookAction struct {
	// Exec runs a shell command inside the sandbox container.
	Exec *ExecHookAction `json:"exec,omitempty"`
	// HTTPPost sends a POST request to an in-sandbox HTTP endpoint via the gateway.
	HTTPPost *HTTPPostHookAction `json:"httpPost,omitempty"`
}

PostStartHookAction describes a single action to execute after a sandbox becomes Running. Exactly one of Exec or HTTPPost should be set (mirrors k8s ProbeHandler style).

type QuotaInfo

type QuotaInfo struct {
	// Name is the Kubernetes object name of the ScitixQuota.
	Name string
	// QuotaURL is the hierarchical path label (quota.scitix.ai/url), e.g. "alice.bob.carol.ted".
	// This value is written as a label on SandboxPool objects.
	QuotaURL string
	// Queue is the scheduling queue type, e.g. "exclusive", "ondemand", "idle".
	Queue string
	// Team is the team this quota belongs to.
	Team string
	// User is the user this quota belongs to.
	User string
	// PoolID is the resource-pool-id label value (string form of the integer ID).
	PoolID string
	// PoolName is the human-friendly name of the resource pool.
	PoolName string
	// Resources is the total hard quota limits (spec.resources).
	Resources map[string]string
	// Used is the currently consumed amount (status.used).
	Used map[string]string
	// Reserved is the reserved but not yet consumed amount (status.reserved).
	Reserved map[string]string
	// Free is the currently free amount from status.statistics.free (may be nil).
	Free map[string]string
}

QuotaInfo is the domain model for a single ScitixQuota.

type RegistryCredential

type RegistryCredential struct {
	Registry string
	Username string
	Password string
}

RegistryCredential is a single registry auth entry.

type Sandbox

type Sandbox struct {
	// SandboxID is the unique identifier for the sandbox, generated at claim time.
	SandboxID string `json:"sandboxId"`
	// Namespace is the Kubernetes namespace of the sandbox Pod.
	Namespace string `json:"namespace"`
	// Team is the team label of the sandbox owner.
	Team string `json:"team,omitempty"`
	// User is the user label of the sandbox owner.
	User string `json:"user,omitempty"`
	// PoolName is the name of the pool the sandbox was claimed from.
	PoolName string `json:"poolName"`
	// PodName is the name of the sandbox Pod.
	PodName string `json:"podName"`
	// Metadata holds arbitrary key-value pairs provided at sandbox creation time.
	Metadata map[string]string `json:"metadata,omitempty"`

	// Status is the current lifecycle status of the sandbox (e.g. Starting, Running, Completed, Failed, etc.).
	Status string `json:"status"`
	// ContainerImages maps container name → image reference (e.g. "python:3.10").
	ContainerImages map[string]string `json:"containerImages,omitempty"`

	// NodeName is the Kubernetes node the sandbox Pod was scheduled onto.
	NodeName string `json:"nodeName,omitempty"`
	// ContainerID is the runtime container ID (e.g. "docker://abc123…") of the first
	// container, captured when the sandbox first entered the Running state.
	// Populated from StableContainerStatuses in the inplace-update-state annotation.
	ContainerID string `json:"containerId,omitempty"`

	// CPU is the sum of all containers' CPU requests (raw K8s string, e.g. "8000m").
	CPU string `json:"cpu,omitempty"`
	// Memory is the sum of all containers' memory requests (raw K8s string, e.g. "8Gi").
	Memory string `json:"memory,omitempty"`

	// ClaimedAt is when the sandbox was claimed and the creation process started.
	ClaimedAt string `json:"claimedAt,omitempty"`
	// StartedAt is when the sandbox entered the Running state, or when a Completed record was persisted to the store.
	StartedAt string `json:"startedAt,omitempty"`
	// TerminatedAt is when the sandbox entered the Stopping state, or when a Failed (evicted/deleted) record was persisted to the store.
	TerminatedAt string `json:"terminatedAt,omitempty"`
	// RecycledAt is when the sandbox finished the Stopping→Idle recycle cycle,
	// or when a Failed (evicted/deleted) record was persisted to the store.
	RecycledAt string `json:"recycledAt,omitempty"`

	// DurationSeconds is the sandbox's wall-clock duration in seconds.
	// Runtime-only: computed at query time from startedAt/terminatedAt, never persisted.
	// Nil for Starting and Canceled states which lack a meaningful start-to-end interval.
	DurationSeconds *int64 `json:"-"`

	// Endpoints maps runtime name → SandboxEndpoint (URL + optional LogDir).
	// Runtime-only: populated from live pool spec, never persisted.
	Endpoints map[string]SandboxEndpoint `json:"-"`

	// StatusDetail holds structured diagnostics for Starting/Failed pods.
	// Runtime-only: derived from live Pod state, never persisted.
	StatusDetail *agentsv1alpha1.SandboxStatusDetail `json:"-"`

	// Historical fields (Completed/Failed/Canceled only):
	FailureReason  string `json:"failureReason,omitempty"`
	ExitCode       *int32 `json:"exitCode,omitempty"`
	FailureMessage string `json:"failureMessage,omitempty"`
}

Sandbox is the core domain model for a claimed sandbox Pod.

Fields with json tags are persisted to the history store (buntdb). Fields tagged json:"-" are runtime-only and never stored.

type SandboxEndpoint

type SandboxEndpoint struct {
	URL    string
	LogDir string // empty if not configured
}

SandboxEndpoint carries the URL and optional log directory for a runtime endpoint.

type SandboxLogEntry

type SandboxLogEntry struct {
	Timestamp time.Time
	Container string
	Log       string
}

SandboxLogEntry is a single parsed log line from a container.

type SandboxLogs

type SandboxLogs struct {
	SandboxID   string
	Namespace   string
	PodName     string
	Entries     []SandboxLogEntry
	TotalBytes  int64
	Source      string // "live" | "runtime"
	RuntimeName string // set when Source == "runtime"
}

SandboxLogs is the domain model returned by GetLogs.

type SandboxPool

type SandboxPool struct {
	Name      string
	Namespace string
	Spec      agentsv1alpha1.SandboxPoolSpec
	Status    agentsv1alpha1.SandboxPoolStatus
	// Overrides stores persisted pool-level overrides that are re-applied during template sync.
	Overrides *PoolTemplateOverrides
	// CPU is the sum of all containers' CPU requests (raw K8s string, e.g. "8000m").
	CPU string
	// Memory is the sum of all containers' memory requests (raw K8s string, e.g. "8Gi").
	Memory string
	// PodDiagnostics holds real-time diagnostic info for Starting/Failed pods.
	// Populated on List (Pod YAML only) and Get (Pod YAML + Events).
	PodDiagnostics []PodDiagnostic
	// Team is the team label of the pool owner (from CRD label).
	Team string
	// User is the user label of the pool owner (from CRD label).
	User string
	// TemplateVersion is the version of the source SandboxTemplate at last sync (from annotation).
	TemplateVersion string
	// CreatedAt is the RFC3339 creation time of the pool (from metadata.creationTimestamp).
	CreatedAt string
	// PoolDocs is the Markdown pool-specific usage docs. Inside the domain layer this
	// field carries the raw template from the linked SandboxTemplate's
	// agentbox.navix.sh/pool-docs annotation. The handler layer substitutes
	// ${poolName}, ${clusterId}, ${apiKey} before serialising to the HTTP response.
	PoolDocs string
}

SandboxPool is the domain model for a SandboxPool CRD.

type SandboxReadinessResult

type SandboxReadinessResult struct {
	SandboxID string
	Ready     bool
	Endpoints map[string]EndpointReadiness
}

SandboxReadinessResult holds the aggregated readiness result for a sandbox.

type SandboxTemplate

type SandboxTemplate struct {
	Name         string
	Version      string
	Description  string
	RuntimeNames []string
	// CPU is the sum of all containers' CPU requests (raw K8s string, e.g. "8000m").
	CPU string
	// Memory is the sum of all containers' memory requests (raw K8s string, e.g. "8Gi").
	Memory string
	// CreatedAt is the RFC3339 creation time of the template.
	CreatedAt string
	// SyncSource is "global" when the template was created/synced via ws-proxy.
	SyncSource string
	// Docs is the Markdown documentation content from the agentbox.navix.sh/docs annotation.
	Docs string
	// CrdYaml is the complete raw CRD YAML without managedFields; includes resourceVersion for optimistic locking.
	CrdYaml string
}

SandboxTemplate is the domain model for a SandboxTemplate CRD.

type SyncSandboxPoolTemplateInput

type SyncSandboxPoolTemplateInput struct {
	Name      string
	Namespace string
}

SyncSandboxPoolTemplateInput carries parameters for syncing a pool's spec from its source template.

type SyncTemplatePreviewResult

type SyncTemplatePreviewResult struct {
	// SpecYaml is the EmbeddedSandboxTemplate YAML after applying all overrides.
	SpecYaml string
	// Version is the version of the source template.
	Version string
}

SyncTemplatePreviewResult is the result of a dry-run SyncTemplate operation.

type UpdateSandboxPoolInput

type UpdateSandboxPoolInput struct {
	Name                   string
	Namespace              string
	Replicas               *int32                                 // nil = don't modify
	MinReplicas            *int32                                 // nil = don't modify
	MaxReplicas            *int32                                 // nil = don't modify
	PodCreationImagePolicy *agentsv1alpha1.PodCreationImagePolicy // nil = don't modify
	OverrideImage          string                                 // empty = don't modify
	Autoscaling            *agentsv1alpha1.PoolAutoscalingSpec    // nil = don't modify
}

UpdateSandboxPoolInput carries parameters for updating an existing SandboxPool.

Jump to

Keyboard shortcuts

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