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

console

package
v0.84.0 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2026 License: Apache-2.0 Imports: 59 Imported by: 0

Documentation

Overview

Package console serves an embedded, read-only, single-operator web UI over a bintrail index. It is the MCP server with a web face: the same query, recovery, status, and metadata engines, reached over HTTP from a browser.

The console serves event browsing, recovery-SQL generation, status, and point-in-time reconstruction. The events API includes connection_id but never query_text/query_hash (see dto.go). No endpoint ever executes SQL; recover generates a script for the operator to review and apply by hand.

Index

Constants

View Source
const (
	// BackupMethodFull is a full backup from the source database (mydumper,
	// or pgbaseline for PostgreSQL): the same job the Create backup button
	// runs. It reads production and needs the console's baseline-creation
	// opt-in; when the server has an S3 destination it uploads there.
	BackupMethodFull = "backup"
	// BackupMethodRefresh rebuilds the newest backup from the recorded change
	// history alone (the same fold as --baseline-refresh-interval), reading
	// nothing from the source. It needs the server's own local directory to
	// write into; the previous backup it reads may be in the bucket, and on
	// a server whose backups go to S3 the scheduled path uploads its result
	// there afterwards (#1539).
	BackupMethodRefresh = "refresh"
)

BackupMethod values name the producer a scheduled run used. They are the wire format of a run's method on the Backups page, not an input.

View Source
const (
	BackupWhyNoIndex     = "this server has no index connection to read the recorded changes from"
	BackupWhyNoLocalDir  = "an update from the recorded changes needs a local backup directory"
	BackupWhyFirstBackup = "no previous backup to update"
	// BackupWhyUnreadablePrefix starts the reason for a full backup taken
	// because the previous one could not be read (the rest names the error).
	BackupWhyUnreadablePrefix = "the previous backup could not be read"
	// BackupWhyFoldRefusedPrefix / BackupWhyFoldCrashedPrefix start the reason
	// for a full backup that stands in for an update the fold refused or that
	// crashed (the rest names the refusal or the panic).
	BackupWhyFoldRefusedPrefix = "the update from the recorded changes was refused"
	BackupWhyFoldCrashedPrefix = "the update from the recorded changes hit an internal error"
	// BackupWhyWindowPrefix / BackupWhyStaleAnchorPrefix start the reason for
	// a full backup chosen over an update the daemon measured (#1721): the
	// update was estimated dearer than a full backup, or its anchor is past
	// the cut-over age with nothing measured. The rest carries the numbers.
	BackupWhyWindowPrefix      = "an update from the recorded changes would take longer than a full backup"
	BackupWhyStaleAnchorPrefix = "the previous backup is too old to update from"
)

The reasons a scheduled run takes a FULL backup instead of updating the previous one (#1604). They are persisted on the run record at run time, never recomputed for a past run: recomputed live, a bucket error that has since cleared shows the cheap producer for a run that read production in full. The first two are permanent until a setting changes, which is why the page turns them into the setting to change.

View Source
const (
	BaselineRunDump    = "dump"    // mydumper (or pgbaseline) snapshot of the source
	BaselineRunRefresh = "refresh" // periodic fold of the newest snapshot forward
	BaselineRunRestore = "restore" // operator-chosen point-in-time fold (#backups)
	BaselineRunCompact = "compact" // table-delta chains merged by the daemon's compaction job (#1723)
)

Kind values for BaselineRunRecord. The literals are the file/wire format.

View Source
const (
	FlavorMySQL    = "mysql"
	FlavorMariaDB  = "mariadb"
	FlavorPostgres = "postgres"
)

Source-family flavors. The registry records which capture engine a source needs: MySQL and MariaDB both index through the go-mysql binlog path (MariaDB differs only by GTID dialect at stream time, --source-flavor mariadb), while PostgreSQL indexes through pgstreamrun. The string values are deliberately the SAME literals stream_state.flavor stores (see internal/query.SourceFlavor and recovery.DialectForFlavor) so a registry-declared flavor and the index-read flavor never disagree. Empty is treated as MySQL so every pre-#1019 registry entry keeps working — the same additive discipline as an empty SSLMode.

View Source
const (
	VerifyTriggerManual    = "manual"
	VerifyTriggerScheduled = "scheduled"
	VerifyStateSkipped     = "skipped"
)

Trigger values for VerifyRunRecord, plus the history-only "skipped" state: live VerifyStatus.State never carries it — only scheduled cycles that could not run record it. Producers must use these consts; the literals are the wire/file format.

View Source
const (
	VerifyStateIdle      = "idle"
	VerifyStateRunning   = "running"
	VerifyStateSucceeded = "succeeded"
	VerifyStateFailed    = "failed"
)

The rest of the VerifyStatus.State vocabulary. Named alongside VerifyStateSkipped because a consumer summarizing the history has to branch on all of them: "not skipped" is NOT "verified" — it also admits failed, and the two transient states a run can be caught in.

View Source
const BackupCutoverMinAge = 2 * time.Hour

BackupCutoverMinAge is the floor of the age rule: an update whose anchor is older than this (or than six schedule intervals, whichever is longer) is replaced by a full backup when nothing was measured. Six intervals so a daily schedule's normal one-day window is never "too old"; two hours so a five-minute schedule that fell hours behind (a long stop, #1721) does not fold the whole gap one page at a time.

View Source
const BackupScheduleMinEvery = 5 * time.Minute

BackupScheduleMinEvery is the shortest interval a schedule accepts: a sanity bound a few ticks above the loop's one-minute clock, not a protection. It was 15m until #1620, when a server that folds from S3 wanted the cadence of a reporting copy. The floor does NOT stop the two costs that number once guarded against: a server with an S3 destination and no local backup directory takes a FULL backup every slot, one with no previous backup takes one on its first (ChooseBackupMethod), and a local-only server keeps every snapshot it publishes. All are said out loud instead, at save and at boot (warnBackupScheduleRate and its twin in backup_schedule_api.go) and on the card as the 30-day count, so the operator reads the rate before the source or the disk does.

View Source
const BaselineRunHistoryCap = 40

BaselineRunHistoryCap is how many baseline runs are kept per server. Old records fall off the front; the history answers "how long did this backup take, and who made it", not "archive every run forever".

View Source
const BaselineRunTriggerScheduled = "scheduled"

BaselineRunTriggerScheduled marks a run (or a skip) the per-server backup schedule started (#1442). Empty is everything else: the Create backup button, a restore, the daemon-wide refresh interval. The literal is the file/wire format and matches the verify history's vocabulary.

View Source
const DaemonBaselineS3Label = "the daemon's --baseline-s3 default"

DaemonBaselineS3Label names the daemon's --baseline-s3 bucket in refusals and warnings about a store claiming it.

View Source
const VerifyHistoryCap = 20

VerifyHistoryCap is how many runs are kept per server. Old records fall off the front — the history answers "when did this last verify, and how has it been trending", not "archive every run forever".

Exported because eviction is SILENT: a consumer summarizing the history has no other way to tell "these are all the runs there ever were" from "these are the newest VerifyHistoryCap of them". A server whose List is exactly this long must be reported as a possibly-truncated window, or a summary says "no failed runs" about a period whose failures fell off the front.

Variables

View Source
var (
	// ErrDuplicateName rejects two registry entries sharing a name — the name
	// is the operator-facing label in the UI switcher, so it must be unique.
	ErrDuplicateName = errors.New("a server with this name already exists")
	// ErrUnknownServer is returned for an id with no registry entry.
	ErrUnknownServer = errors.New("unknown server id")
	// ErrRegistryReadOnly is returned for mutating operations when the on-disk
	// file was written by a newer bintrail (version > registryVersion).
	ErrRegistryReadOnly = errors.New("server registry was written by a newer bintrail; upgrade bintrail to edit it")
	// ErrS3StoreConflict: two servers name the same bucket but route it to
	// different stores (#1575). The store is per bucket, so one of them would
	// silently get the other's endpoint; the edit is refused instead.
	ErrS3StoreConflict = errors.New("S3 store conflict")
)
View Source
var ErrAuthFileReadOnly = errors.New("auth file was written by a newer bintrail; logins work but changes are refused")

ErrAuthFileReadOnly is returned by write paths when the on-disk file was written by a newer bintrail (version > authFileVersion).

View Source
var ErrBackupScheduleNotRunnable = errors.New("the schedule cannot run")

ErrBackupScheduleNotRunnable is the class of every "this schedule cannot run on this daemon as configured" refusal; errors.Is matches it. The reason is carried separately (ScheduleRefusal) because it is what the page shows.

View Source
var ErrBaselineRunning = errors.New("a baseline is already running for this server")

ErrBaselineRunning is returned by BaselineController.Trigger when a baseline for that server is already in flight (one at a time per server). The handler maps it to 409 Conflict.

View Source
var ErrExplainRunning = errors.New("the drill-down for this table is still being computed")

ErrExplainRunning is returned by VerifyController.Explain while the drill-down for that table is still being computed. The handler maps it to 202 Accepted, and the caller polls the same URL until it answers 200, 404 (a newer run replaced the pair), or the caller's own cap.

This exists because the drill-down RE-RECONSTRUCTS the table (#1375): on a large table that is minutes of DuckDB work. The console itself tolerates that — it sets no WriteTimeout and apiGuard clears the read deadline — but a fronting reverse proxy at its stock read timeout (60s on nginx) does not, so behind one the operator's only experience was a request that died in the proxy: a button that silently did nothing. The work now runs on the daemon like a verify run does, and the request returns immediately either way.

View Source
var ErrExplainUnavailable = errors.New("no explainable mismatch for this table: run a baseline-anchored verify first; or this table was not reported as a mismatch by the last run; or a newer run has replaced the previous results (re-open Explain if that run still reports this table as a mismatch)")

ErrExplainUnavailable is returned by VerifyController.Explain when there is no cached baseline pair to explain from: no completed baseline-anchored run exists for this server, the table was not reported as a mismatch by it, or a newer run has since replaced it. The handler maps it to 404. The message names the newer-run case explicitly: a scheduled run (--verify-interval) that starts while a drill-down is still computing drops the previous run's cached pairs, so an operator mid-wait can land here with a verify actually in flight. Without that clause the text tells them to "run a baseline-anchored verify first" while one is running — advice that sends them looking for a problem that does not exist.

View Source
var ErrMCPTokenFileReadOnly = errors.New("MCP token file was written by a newer bintrail; changes are refused")

ErrMCPTokenFileReadOnly is returned by write paths when the on-disk file was written by a newer bintrail (version > mcpTokenFileVersion).

View Source
var ErrSchemaSnapshotRunning = errors.New("a schema snapshot is already running for this server")

ErrSchemaSnapshotRunning is returned by SchemaSnapshotController.Trigger when one is already in flight for that server (one at a time per server). The handler maps it to 409 Conflict.

View Source
var ErrVerifyRunning = errors.New("a verify run is already running for this server")

ErrVerifyRunning is returned by VerifyController.Trigger when a verify run for that server is already in flight (one at a time per server, mirroring BaselineController). The handler maps it to 409 Conflict.

Functions

func BackupCutoverAge added in v0.84.0

func BackupCutoverAge(interval time.Duration) time.Duration

BackupCutoverAge is the anchor age past which the age rule cuts over, for a schedule that fires every interval (zero interval: the floor).

func BackupWhyCode added in v0.82.0

func BackupWhyCode(why string) string

BackupWhyCode classifies a persisted full-backup reason for the page, so the remedy is chosen by a stable code and not by matching prose that may be reworded later. Classified when the record is WRITTEN, so an old record keeps the code its wording had then; the loop's live view of a job classifies the same captured wording again in the same build, which is the only surface for a full backup that panicked and wrote no record. Empty for a reason this build does not know (an update, or a newer daemon's wording).

func BaselineFoldSource added in v0.74.0

func BaselineFoldSource(e ServerEntry) string

BaselineFoldSource is where a fold for e reads its PREVIOUS snapshot from, which is not always where it writes the new one.

The two diverge exactly when backups go to S3 (#1539): the previous snapshot lives in the bucket, so a fold that read the local directory would find an empty one on a server whose backups have only ever been uploaded, and report "no previous backup to update" while the bucket holds dozens. FindBaseline and ListBaselines both dispatch on the s3:// prefix, so the remote source needs no separate code path here.

Note this is NOT how time-travel resolves a baseline: bundle.findBaseline prefers the local directory and treats the bucket only as a per-table fallback (#766), so on a server with both, a table whose local copy is stale is read stale by time-travel and fresh by the fold. The Backups listing merges both locations (#1571) and the point-in-time restore reads this same source (#1541); do not "fix" the divergence by making the fold read local. The daemon's refresh loop DOES read the local copy for one run when that copy is the bucket's newest snapshot, table for table (consoleapp resolveFoldSource, #1626): never a stale copy, only the same one, so unchanged tables can be carried forward by hard link.

The OUTPUT stays local whatever this returns: the fold writes Parquet to a filesystem, and the upload is a separate step afterwards.

func CheckBackupSchedule added in v0.71.0

func CheckBackupSchedule(e ServerEntry, sched BackupSchedule, gates BackupScheduleGates) error

CheckBackupSchedule reports whether e's schedule can run on a daemon with these gates, and why not. Nil error means at least one producer can run. Called on PUT (so a schedule that could never run is refused with the reason instead of saved) and on every listing (so a schedule the environment later invalidated is reported, not silently skipped by the loop).

func ChooseBackupMethod added in v0.71.0

func ChooseBackupMethod(ctx context.Context, e ServerEntry, gates BackupScheduleGates) (method, why string, err error)

ChooseBackupMethod decides how the next scheduled run for e will be made, and why, on a daemon with these gates. The rule, in order:

  • a server an update cannot serve (no index DSN, no local backup directory) gets a FULL backup, with that refusal as the why;
  • a server with no previous backup gets a FULL backup: there is nothing to update. A directory that does not exist yet is that case; one that cannot be READ is its own error, never "no backup yet". The previous backup is looked for where the fold would READ it (BaselineFoldSource), which is the bucket on an S3-backed server;
  • a server whose update the daemon measured as dearer than a full backup, or whose previous backup is past the cut-over age with nothing measured (#1721, CutoverToFull), gets a FULL backup with the numbers as the why — only when a full backup can start; otherwise the update runs, slow as it may be, because it is the producer that can;
  • otherwise the newest backup is UPDATED from the recorded changes, with no load on the source. If that update fails (a capture gap, a schema change, a crash), the loop takes a full backup at the same slot when the daemon may take one, and records a skip naming both otherwise.

A rule that picks a producer the daemon cannot run (the opt-in off, no source) is reported as such; the caller decides whether that is a skip.

func ChooseBackupMethodAt added in v0.84.0

func ChooseBackupMethodAt(ctx context.Context, e ServerEntry, gates BackupScheduleGates, now time.Time) (method, why string, err error)

ChooseBackupMethodAt is ChooseBackupMethod deciding for a slot at now: the loop passes its tick, so the age rule reads the slot's clock and a test can pin it.

func CutoverToFull added in v0.84.0

func CutoverToFull(w BackupWindow, interval time.Duration, now time.Time) string

CutoverToFull is the #1721 rule: the why for a FULL backup instead of an update, or "" to update. Measured first: with the events since the anchor, a measured fold rate and a last full backup on record, the update is cut over when its estimate exceeds the full backup's duration, and NOT cut over otherwise, however old the anchor (the estimate is the better evidence). Only without one of the three does the age rule apply: an anchor older than BackupCutoverAge(interval). The estimate is crude on purpose (events × rate, as the issue asked): it decides between two producers, not a schedule.

func DefaultAuthPath added in v0.11.0

func DefaultAuthPath() string

DefaultAuthPath returns ~/.config/bintrail/console-auth.yaml, with the same homeless fallback as DefaultRegistryPath (see configPath).

func DefaultBaselineHistoryPath added in v0.65.0

func DefaultBaselineHistoryPath(serversPath string) string

DefaultBaselineHistoryPath returns the history file path as a sibling of the server registry file, so --console-servers-file relocations carry it.

func DefaultMCPTokenPath added in v0.40.0

func DefaultMCPTokenPath() string

DefaultMCPTokenPath returns ~/.config/bintrail/console-mcp-token.yaml, with the same homeless fallback as DefaultAuthPath (see configPath).

func DefaultRegistryPath

func DefaultRegistryPath() string

DefaultRegistryPath returns ~/.config/bintrail/console-servers.yaml, with configPath's working-directory fallback for homeless environments. The verify and baseline run histories are named as siblings of this path, so they inherit whatever it resolves to.

func DefaultVerifyHistoryPath added in v0.49.0

func DefaultVerifyHistoryPath(serversPath string) string

DefaultVerifyHistoryPath returns the history file path as a sibling of the server registry file, so `--console-servers-file` relocations carry both.

func FullBackupPossible added in v0.71.0

func FullBackupPossible(e ServerEntry, gates BackupScheduleGates) error

FullBackupPossible reports whether a full backup can start for e on a daemon with these gates, and why not. Exported for the loop's fallback decision, which needs exactly this half of ChooseBackupMethod.

func NormalizeFlavor added in v0.33.0

func NormalizeFlavor(s string) (string, error)

NormalizeFlavor maps a request/registry flavor string to a canonical value. Empty → mysql; "postgresql" is accepted as an alias for "postgres". An unrecognized value is an error so a create/update rejects a typo rather than silently mis-routing capture.

func PGReplDSN added in v0.33.0

func PGReplDSN(queryDSN string) (string, error)

PGReplDSN derives the replication (walsender) connection string from a stored PostgreSQL query DSN by adding replication=database. pgcapture/pgstreamrun and pgbaseline require a distinct replication connection (a CopyBoth conn that cannot run ordinary SQL); the console stores only the query DSN and derives this one, so the replication-param logic lives in exactly one place. Consumed by the monitor supervisor (#1020) and the baseline trigger (#1021).

Pure net/url — no pgx: internal/console is on the read side of the dependency-guard (internal/event/depguard_test.go) and must never link the PostgreSQL capture libraries. A postgres:// URL round-trips through net/url and re-parses cleanly in pgconn.ParseConfig downstream.

func RefusalReason added in v0.71.0

func RefusalReason(err error) string

RefusalReason returns the reason inside a ScheduleRefusal, or the whole message for any other error.

func RevokeMCPToken added in v0.40.0

func RevokeMCPToken(path string) error

RevokeMCPToken removes the managed-token file. A missing file is a no-op, a MALFORMED file is removed with a warning (revoking junk is still revoking), an unreadable file or a newer-version file refuses — deleting requires only directory permission, so an unreadable file could otherwise be destroyed without ever checking the read-only contract.

func SetAuthPassword added in v0.11.0

func SetAuthPassword(path, username, password string) error

SetAuthPassword hashes password (bcrypt, cost consoleBcryptCost) and writes the credential file atomically, preserving any Extra fields a newer binary stored. username == "" keeps the existing username (default "admin" on first set). Refuses a newer-version file with ErrAuthFileReadOnly.

func ValidateNewPassword added in v0.11.0

func ValidateNewPassword(password string) error

ValidateNewPassword enforces the set-time policy: at least minPasswordChars characters and at most 72 bytes (bcrypt's hard limit — reject, never truncate).

Types

type AuthFile added in v0.11.0

type AuthFile struct {
	Version        int    `yaml:"version"`
	Username       string `yaml:"username"`
	PasswordBcrypt string `yaml:"password_bcrypt"`
	UpdatedAt      string `yaml:"updated_at"`
	// Extra preserves unknown top-level fields a newer binary wrote, across
	// this binary's load→save cycle (registry.go precedent; inline requires
	// map[string]any).
	Extra map[string]any `yaml:",inline"`
	// contains filtered or unexported fields
}

AuthFile is the on-disk console credential: one username and one bcrypt hash. The console is single-user by design (multi-user/RBAC is dbtrail territory); the versioned envelope and Extra leave room for additive fields without a format break.

func LoadAuthFile added in v0.11.0

func LoadAuthFile(path string) (*AuthFile, error)

LoadAuthFile reads the credential file at path. A missing file returns (nil, nil): password login not configured. A present-but-unreadable or unparseable file is a loud error — silently degrading to token-only when the operator configured a password would be an auth downgrade.

func (*AuthFile) ReadOnly added in v0.11.0

func (a *AuthFile) ReadOnly() bool

ReadOnly reports whether write paths must refuse (newer-version file).

func (*AuthFile) VerifyPassword added in v0.11.0

func (a *AuthFile) VerifyPassword(username, password string) bool

VerifyPassword checks username+password against the stored credential in constant time: the username compare is subtle.ConstantTimeCompare and a username mismatch STILL runs a full bcrypt compare (against the dummy hash) so timing cannot enumerate the username. Safe on a nil receiver (false).

type BackupSchedule added in v0.71.0

type BackupSchedule struct {
	// Every is the interval: Nm, Nh or Nd (cliutil.ParseInterval).
	Every string `yaml:"every"`
	// At is the UTC clock time the grid is aligned to, HH:MM. Empty means
	// 00:00. For whole days (1d, 7d) it is the time of day the backup runs;
	// for an interval that divides a day (6h, 15m) it is the alignment of the
	// slots; an interval that does not divide a day evenly (5h, 36h) drifts
	// through the day, and the page shows the next run so that is visible.
	At string `yaml:"at,omitempty"`
	// Extra preserves future keys the way ServerEntry.Extra does.
	Extra map[string]any `yaml:",inline"`
}

BackupSchedule is the per-server schedule as stored in the registry. The fields hold the operator-typed strings so they round-trip exactly; Parse turns them into durations with the same grammar as the daemon flags.

func (BackupSchedule) Identity added in v0.71.0

func (b BackupSchedule) Identity() string

Identity is the schedule as a comparable string (every|at, with the defaults resolved), for a consumer that must notice a CHANGED schedule: the loop treats an edit as a new schedule, so the "first observation is silent" rule covers edits the way it covers adds. Unparseable schedules identify by their raw fields.

func (BackupSchedule) Normalized added in v0.71.0

func (b BackupSchedule) Normalized() (BackupSchedule, error)

Normalized returns the schedule with its defaults spelled out, for storage: "" becomes "00:00", so the file says what runs.

func (BackupSchedule) Parse added in v0.71.0

Parse validates the schedule and resolves its defaults. Every error names the field and the accepted grammar, because the message is what the Backups page shows back.

type BackupScheduleGates added in v0.71.0

type BackupScheduleGates struct {
	// LoopRunning: this process runs the schedule loop at all (a watch daemon
	// with a baseline supervisor).
	LoopRunning bool
	// FullBackups: this process may take full backups from the source (the
	// baseline-creation opt-in). A rebuild does not need it.
	FullBackups bool
	// FullBackupsErr, when set, is why a full backup cannot START even with
	// the opt-in on: the lock-mode misconfiguration the supervisor refuses
	// every MySQL dump with.
	FullBackupsErr string
	// ReadOnlyConsole: this process is the standalone `serve` console, which
	// runs no loop of any kind; names the daemon in the reason.
	ReadOnlyConsole bool
	// Window measures what an update would have to fold for one server, so
	// ChooseBackupMethod can cut over to a full backup when the update is
	// the dearer producer (#1721). Nil: never cut over (a process with no
	// measurements, or a test).
	Window BackupWindowProbe
}

BackupScheduleGates is what the daemon can do, as the schedule checker needs to know it. All of it is decided at boot.

type BackupScheduleReporter added in v0.71.0

type BackupScheduleReporter interface {
	ScheduleState(serverID string) BackupScheduleState
	// FullBackups reports whether the loop may run full backups (the
	// daemon's baseline-creation opt-in), and, when it may, whether the
	// supervisor would still refuse to start one (a lock-mode
	// misconfiguration): nil when full backups can start.
	FullBackups() (enabled bool, refusal error)
	// Observe tells the loop a schedule was saved at `at`, so the slot in
	// progress then counts as seen and the NEXT boundary fires, even one
	// that falls before the loop's next tick. Without it the next_run the
	// page showed at save time could be silently skipped.
	Observe(serverID string, sched BackupSchedule, at time.Time)
	// Forget tells the loop the schedule for serverID was removed, so its
	// observation and last outcome are dropped now, not at the next tick.
	Forget(serverID string)
	// WindowProbe measures what an update would fold (#1721), for
	// ChooseBackupMethod; nil when this process measures nothing.
	WindowProbe() BackupWindowProbe
}

BackupScheduleReporter is the schedule loop as the console sees it. nil when this process runs no loop (the read-only console, or a watch daemon with every baseline feature off), which the schedule endpoints refuse on and the listing reports as not runnable.

type BackupScheduleState added in v0.71.0

type BackupScheduleState struct {
	// LastStartedAt is when the loop last started a job for this server
	// (RFC3339 UTC), empty if never in this process.
	LastStartedAt string
	// LastMethod is that job's producer (BackupMethodFull/Refresh).
	LastMethod string
	// LastWhy is why that job was a full backup (#1604), as decided when it
	// started; empty for an update.
	LastWhy string
	// Last is the supervisor's status for THAT job: live while it is in the
	// slot, afterwards the copy ScheduleState took when it saw the job
	// terminal (so a later manual job taking the slot does not erase it),
	// nil when nothing was started here. The copy is taken on READ; the
	// loop's watcher is the read that always happens, so it exists for
	// every scheduled job, not only the ones a page load caught in time.
	Last *BaselineStatus
	// Running: the job this schedule last started has not finished.
	Running bool
	// LastSkippedAt / LastSkipReason describe the last slot this process
	// could not start, empty if none. The history has the durable copy.
	LastSkippedAt  string
	LastSkipReason string
	// LastFallbackAt / LastFallbackReason describe the last slot where the
	// update from the recorded changes failed and a full backup was STARTED
	// in its place (never a collision, which is a skip); cleared when a
	// later scheduled job other than that full backup succeeds. This
	// process only.
	LastFallbackAt     string
	LastFallbackReason string
}

BackupScheduleState is the schedule loop's in-memory view of one server, for the Backups page: the job the schedule last started in this process, as the supervisor's slot reports it. The durable view (runs and skips across restarts) is the baseline run history; this one exists because the history can be unavailable (an unreadable file) or silent (a job that panicked writes no record), and a failed scheduled backup must reach the page either way.

type BackupSettingsDefaults added in v0.77.0

type BackupSettingsDefaults struct {
	BaselineRetain string // --baseline-retain
	RefreshEvery   string // --baseline-refresh-interval
	LockMode       string // BINTRAIL_CONSOLE_BASELINE_LOCK_MODE
	// LockModeErr: the env value was rejected, so LockMode holds the
	// fallback default, which is NOT in force — MySQL dumps are refused
	// while it stands. The page must show the rejection, not the fallback.
	LockModeErr    string
	TriggerOn      bool   // BINTRAIL_CONSOLE_BASELINE_TRIGGER
	StagingDir     string // BINTRAIL_CONSOLE_BASELINE_STAGING
	VerifyInterval string // --verify-interval
	VerifyTables   string // --verify-tables
}

BackupSettingsDefaults carries the daemon-wide flag/env values the page reports, injected by the watch daemon exactly like RotationDefaults: what the process was TOLD, verbatim, so the page never re-derives it. Zero on the standalone serve, whose page hides the daemon card (no monitor capability); the per-server half of the page renders there regardless.

type BackupWindow added in v0.84.0

type BackupWindow struct {
	// Anchor is the previous snapshot's instant (the one an update would
	// start from); zero when unknown.
	Anchor time.Time
	// Events is how far the index's high-water mark has moved since the
	// anchor; negative when unknown (no mark on record for THIS anchor, or
	// the index did not answer). It counts every source writing to that
	// index and any rows a resumed capture re-inserted, not the rows this
	// server's fold would apply: the same over-count is in the rate, so the
	// two cancel while the traffic mix holds.
	Events int64
	// FoldFixed and FoldRate model what an update costs from the run
	// history: fixed seconds every update pays (starting DuckDB, reading
	// every table's previous Parquet, fetching it from the bucket) plus one
	// second per FoldRate events. Zero when unknown: a rate needs at least
	// two measured updates whose times differ, or the per-event cost is
	// not distinguishable from the fixed one (a quiet server's updates all
	// take the fixed cost, and a rate read off them would be an artefact
	// that cuts over on every burst).
	FoldFixed time.Duration
	FoldRate  float64
	// Proven is the largest number of events a recorded update applied in
	// less time than the last full backup took. An update that small has
	// been done cheaper, so it is never cut over: evidence beats the model.
	Proven int64
	// LastFull is how long the last successful full backup took; zero when
	// there is none on record.
	LastFull time.Duration
}

BackupWindow is what a scheduled update would have to fold, as the daemon measured it. Every field may be unknown, and the rule (CutoverToFull) says which combinations it can decide on.

type BackupWindowProbe added in v0.84.0

type BackupWindowProbe func(ctx context.Context, e ServerEntry, anchor time.Time) BackupWindow

BackupWindowProbe measures the window for e whose previous snapshot is anchor. It runs on every decision, page loads included, so it must be cheap and must answer "unknown" rather than block.

type BaselineController added in v0.21.0

type BaselineController interface {
	// Trigger starts a baseline in the background and returns immediately.
	// Returns ErrBaselineRunning if one is already running for req.ServerID.
	Trigger(req BaselineRequest) error
	// Status reports the latest known state for a server (idle if never run in
	// this process — the durable record is the snapshot itself, listed by
	// /api/baselines).
	Status(serverID string) BaselineStatus
}

BaselineController runs a baseline snapshot for a monitored server entirely in-process — dump → convert → upload — so the console never shells a sibling container and never mounts the docker socket (#613). It is wired in ONLY by `bintrail-console watch` when the operator opts in (BINTRAIL_CONSOLE_BASELINE_TRIGGER=1); nil on the standalone read-only console, where the endpoint refuses with 403 — mirroring how MonitorController gates the monitor verbs.

type BaselineRefreshConfig added in v0.70.0

type BaselineRefreshConfig struct {
	// CarryForwardUnchanged publishes a table with no events in the window by
	// carrying its previous Parquet file forward instead of rewriting it. Off
	// by default: the rows are identical either way, but the on-disk
	// representation is not. Where the filesystem allows it the two snapshots
	// end up sharing one inode, so a prune reports space it will not reclaim
	// while the newer snapshot references the file. Separately, and for a
	// different reason, the carried table stays anchored at its older binlog
	// coordinate, which is correct rather than a cost: its deltas resume
	// exactly there.
	CarryForwardUnchanged bool `yaml:"carry_forward_unchanged"`
}

BaselineRefreshConfig is the console-editable half of the baseline refresh loop's configuration. The interval itself stays a daemon flag: it decides whether the loop runs at all, and starting a loop that was not booted takes a restart. What is here applies to a loop already running, because every cycle re-reads the registry.

type BaselineRefreshDefaults added in v0.70.0

type BaselineRefreshDefaults struct {
	// CarryForwardUnchanged is the daemon's --baseline-carry-forward-unchanged.
	CarryForwardUnchanged bool
	// Enabled reports whether ANY consumer of the setting is live in this
	// daemon, which is what decides if a saved value has an effect today.
	//
	// Two consumers read it, and only one is the loop. The point-in-time
	// restore folds into the same store with the same setting, and it is wired
	// whenever a baseline supervisor exists, which --baseline-trigger alone is
	// enough to create. Reading liveness off the refresh interval alone told an
	// operator running --baseline-trigger that nothing was live, and then a
	// Restore hard-linked their files. For a setting whose whole purpose is
	// taking consent about the on-disk representation, that is the one thing
	// the panel must not do.
	Enabled bool
	// Scheduled reports the narrower fact: a periodic refresh loop is running
	// (--baseline-refresh-interval was set). Separate from Enabled because the
	// panel's copy has to distinguish "this applies to your restores but
	// nothing is on a timer" from "this applies to nothing until a restart".
	Scheduled bool
}

BaselineRefreshDefaults is the daemon-side baseline-refresh configuration, surfaced to the settings panel as the fallback when nothing is saved.

type BaselineRefreshReporter added in v0.50.0

type BaselineRefreshReporter interface {
	// RefreshStatus reports the latest periodic refresh for a server, or state
	// "idle" when none has run here. Kept apart from BaselineController.Status
	// because the two answer different questions — "did my dump work" and "is my
	// baseline still moving forward on its own" — and one shared slot would let
	// a manual dump erase the evidence that the automatic refresh has been
	// failing for a week.
	RefreshStatus(serverID string) BaselineStatus
}

BaselineRefreshReporter reports the daemon's periodic baseline refresh (#1171). Deliberately a SEPARATE interface from BaselineController, wired from a separate Config field, because the two features are independently opt-in: the manual dump needs mydumper and BINTRAIL_CONSOLE_BASELINE_TRIGGER=1, while a refresh needs neither — it exists precisely so a fresher baseline does not require a dump. Folding the report into BaselineController would gate the refresh behind the dump's opt-in, or (worse) un-gate the Create-baseline button for anyone who enabled only the refresh.

nil when the daemon runs no refresh loop.

type BaselineRequest added in v0.21.0

type BaselineRequest struct {
	ServerID   string
	ServerName string
	SourceDSN  string
	Schemas    []string
	// LocalDir is the per-server baseline directory (entry.BaselineDir). When
	// set, the snapshot is written there persistently and NOT uploaded. Empty
	// means S3-only: stage in a temp dir, upload to S3, discard the staging.
	LocalDir string
	// S3 is the per-server baseline S3 prefix (entry.BaselineS3, s3://…). When
	// set, the staged snapshot is uploaded there. Region/credentials come from
	// the ambient AWS chain (env / ~/.aws / IAM role), like the rest of the
	// console's S3 access.
	S3 string
	// Flavor selects the baseline producer: "postgres" runs internal/pgbaseline
	// (COPY + the slot's pgoutput anchor); anything else runs mydumper. Plain
	// strings keep internal/console pgx-free (the read-layer dependency guard).
	Flavor string
	// Slot / Publication are the PostgreSQL replication slot and publication
	// (postgres flavor only); the producer stamps the snapshot at the slot's
	// consistent-point LSN so Time-travel/reconstruct can replay deltas from it.
	// Empty for MySQL/MariaDB.
	Slot        string
	Publication string
	// Trigger is stamped onto the run's history record:
	// BaselineRunTriggerScheduled when the backup schedule started this job,
	// empty for the Create backup button.
	Trigger string
	// Why is the reason the backup schedule chose a FULL backup for this
	// run (#1604), persisted on the run record so the page can say what was
	// missing; empty for a manual backup.
	Why string
}

BaselineRequest is the in-process job description the endpoint hands the controller. The source DSN (a secret) stays inside the process — it is never written to disk or serialized to any HTTP response.

func BaselineRequestFor added in v0.71.0

func BaselineRequestFor(e ServerEntry) BaselineRequest

BaselineRequestFor builds the in-process job description for a registry entry. One constructor for the button and the schedule, so the two can never dump a different schema set or destination for the same server.

type BaselineRestoreRequest added in v0.65.0

type BaselineRestoreRequest struct {
	ServerID   string
	ServerName string
	IndexDSN   string
	// BaselineDir is the server's own local directory: where the fold WRITES,
	// and where it reads the backup to fold from when BaselineS3 is empty.
	BaselineDir string
	// BaselineS3 is the server's configured S3 destination, or empty. When
	// set, the backup to fold from is looked for in the bucket (the
	// BaselineFoldSource rule) and the finished snapshot is uploaded there, so the restore
	// behaves like the scheduled update on the same server (#1541). Before it
	// was carried, the restore listed the local directory alone, which on an
	// S3-backed server holds only what this daemon folded since it started,
	// and refused with "no backup exists" while the bucket held dozens.
	BaselineS3 string
	At         time.Time
	// CarryForwardUnchanged is the effective setting the console resolved for
	// this restore. A restore is the same fold the refresh performs, into the
	// same store, so it honours the same operator choice; leaving it out is
	// how the two silently diverged.
	CarryForwardUnchanged bool
}

BaselineRestoreRequest identifies the server and the instant to restore to.

type BaselineRestorer added in v0.65.0

type BaselineRestorer interface {
	// TriggerRestore starts the restore asynchronously. ErrBaselineRunning
	// when another baseline job for the server is in flight.
	TriggerRestore(req BaselineRestoreRequest) error
	// RestoreStatus reports the last restore for a server (idle if none).
	RestoreStatus(serverID string) BaselineStatus
}

BaselineStatus is the pollable state of a server's most recent baseline job. BaselineRestorer runs an operator-chosen point-in-time restore: fold the snapshot at-or-before At forward through the index's deltas and publish the result as a NEW discoverable snapshot in the same baseline store (the backups page's PITR action). Deliberately a separate interface from BaselineController and BaselineRefreshReporter: the three features are independently wired, and deriving one from another has already produced a daemon that refused to start (#1171's lesson).

type BaselineRunHistory added in v0.65.0

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

BaselineRunHistory is the persisted baseline-run history: one JSON file, capped per server, written atomically like the server registry and the verify history. Console-local state on disk, deliberately NOT a table in the index database (registry DSNs never receive DDL).

func OpenBaselineHistory added in v0.65.0

func OpenBaselineHistory(path string) (*BaselineRunHistory, error)

OpenBaselineHistory loads the history at path. A missing file is an empty history; a corrupt or newer-versioned file is an error for the caller to decide on, never silently truncated.

func (*BaselineRunHistory) Append added in v0.65.0

Append records one run and saves the file (oldest dropped past the cap). A save failure is returned for the caller to log; history is an observability aid and must never fail the run it describes.

func (*BaselineRunHistory) AppendCompact added in v0.84.0

func (h *BaselineRunHistory) AppendCompact(rec BaselineRunRecord) (bool, error)

AppendCompact records a table-delta compaction run (#1723). When the run failed and the newest record for the server is a compaction that failed the same way, that record's end moves to this run instead of a new record being added, for the reason AppendSkip gives: the job is retried at every refresh, so a persistent failure would otherwise append an identical record every cycle and push the refreshes off the capped history. A success, or a different error, is a new record. Returns whether a NEW record was added.

func (*BaselineRunHistory) AppendSkip added in v0.71.0

func (h *BaselineRunHistory) AppendSkip(rec BaselineRunRecord) (bool, error)

AppendSkip records a scheduled slot that did not start. When the newest record for the server is already the same skip, that record's FinishedAt moves to this slot instead of a new record being added: a wedged job plus a short interval would otherwise append an identical skip every slot and push the durations ledger out of the capped history, while a frozen timestamp would have the page report the FIRST missed slot of a streak as if the streak had ended there. Returns whether a NEW record was added.

func (*BaselineRunHistory) FindBySnapshot added in v0.65.0

func (h *BaselineRunHistory) FindBySnapshot(serverID, snapshotTime string) *BaselineRunRecord

FindBySnapshot returns the newest record for serverID whose SnapshotTime equals snapshotTime (RFC3339 UTC), or nil.

func (*BaselineRunHistory) IndexMarkFor added in v0.84.0

func (h *BaselineRunHistory) IndexMarkFor(serverID, snapshotTime string) (uint64, bool)

IndexMarkFor is the index high-water mark read before the successful update that published the snapshot named snapshotTime (RFC3339 UTC), and whether one is on record: the base an update from that snapshot counts its events from, after a restart emptied the in-memory memo.

func (*BaselineRunHistory) LastFullBackup added in v0.84.0

func (h *BaselineRunHistory) LastFullBackup(serverID string) time.Duration

LastFullBackup is how long the newest successful full backup for serverID took, start to finish (the upload included: that is what the schedule waits for); zero when there is none on record or its stamps do not parse.

func (*BaselineRunHistory) LastScheduled added in v0.71.0

func (h *BaselineRunHistory) LastScheduled(serverID string) (run, skip *BaselineRunRecord)

LastScheduled returns the newest scheduled RUN for serverID and the newest scheduled SKIP, either nil when there is none. Both, because they answer different questions on the Backups page: "when did the schedule last produce a backup" and "is it currently unable to". A skip newer than the last run is the case the page has to shout about.

func (*BaselineRunHistory) List added in v0.65.0

func (h *BaselineRunHistory) List(serverID string) []BaselineRunRecord

List returns a copy of serverID's records, oldest first.

func (*BaselineRunHistory) ProvenUpdate added in v0.84.0

func (h *BaselineRunHistory) ProvenUpdate(serverID string, within time.Duration) int64

ProvenUpdate is the most events any recorded successful update for serverID applied in less than within: what the history proves an update can do cheaper than that. Zero when nothing on record qualifies.

func (*BaselineRunHistory) UpdateModel added in v0.84.0

func (h *BaselineRunHistory) UpdateModel(serverID string) (fixed time.Duration, rate float64)

UpdateModel fits what an update costs for serverID from its last few measured successful updates: fixed seconds every update pays, taken as the shortest run in the sample, plus a rate in events per second from what the OTHER runs applied and took BEYOND that run (its events are paid for inside the fixed cost, so counting them again would read the rate too fast and the estimate too cheap). A run that took longer but applied no more events (a slow disk that day) says nothing about the per-event cost and is left out. The rate is zero (unknown) with fewer than two samples, or when the time beyond the fixed cost is under a tenth of the sample's total: then every update cost about the same whatever it applied, the per-event cost is not distinguishable, and a rate read off such runs would be a small number that makes any burst look like hours.

type BaselineRunRecord added in v0.65.0

type BaselineRunRecord struct {
	ServerID   string `json:"server_id"`
	ServerName string `json:"server_name,omitempty"`
	Kind       string `json:"kind"`
	// Trigger is BaselineRunTriggerScheduled for the backup schedule's own
	// runs and skips, empty otherwise.
	Trigger string `json:"trigger,omitempty"`
	// SkipReason is set on a scheduled slot that did not start; see above.
	SkipReason string `json:"skip_reason,omitempty"`
	// SnapshotTime is the published snapshot's anchor instant — its directory
	// name — in RFC3339 UTC. Empty when the run failed before publishing, or
	// when the producer does not report it (PostgreSQL dumps stamp the
	// snapshot server-side, out of this process's sight).
	SnapshotTime string `json:"snapshot_time,omitempty"`
	StartedAt    string `json:"started_at"`
	FinishedAt   string `json:"finished_at"`
	Tables       int    `json:"tables,omitempty"`
	// Carried counts tables published by reusing the previous snapshot's file
	// (refresh and restore). Persisted rather than left to the live status,
	// which the next run overwrites: whether a run cost a full rewrite is
	// exactly the thing an operator looks back at when sizing a disk or an
	// interval, and by then the live status is gone.
	Carried int `json:"carried,omitempty"`
	// CarriedCopied narrows Carried to the reuses published as full byte
	// copies (no hard link, no disk saved) — see BaselineStatus.CarriedCopied.
	CarriedCopied int    `json:"carried_copied,omitempty"`
	Rows          int64  `json:"rows,omitempty"`
	Uploaded      int    `json:"uploaded,omitempty"`
	Refused       int    `json:"refused,omitempty"`
	Error         string `json:"error,omitempty"`
	// Why is the reason a scheduled run was a FULL backup rather than an
	// update, as decided when it ran (#1604); WhyCode is BackupWhyCode of
	// it, fixed at write time. Empty on updates and manual backups.
	Why     string `json:"why,omitempty"`
	WhyCode string `json:"why_code,omitempty"`
	// Events is how far the index high-water mark moved between this
	// daemon's previous fold of the snapshot an update started from and
	// this one (every source writing to the index counts, and so do rows a
	// resumed capture re-inserted), UpdateSeconds how long the whole update
	// run took, upload included, like the full backup duration it is
	// compared against (#1721), and IndexMark the high-water mark read
	// before the fold: the base the NEXT update's Events are counted from,
	// kept here so the count survives a daemon restart. Zero when not
	// measured (a full backup, a restore, a fold with no previous mark).
	Events        int64   `json:"events,omitempty"`
	UpdateSeconds float64 `json:"update_seconds,omitempty"`
	IndexMark     uint64  `json:"index_mark,omitempty"`
}

BaselineRunRecord is one completed baseline-producing run as this daemon performed it. The files listing joins it to a snapshot by SnapshotTime to report the run's exact duration; snapshots produced elsewhere (the CLI, another daemon) have no record and fall back to the file write span.

A record with SkipReason set is NOT a run: it is a scheduled slot that could not start (another backup job held the server, or the schedule was not runnable). It has no snapshot, so the files listing never joins it; it exists so a schedule that never gets to run stays visible on the Backups page instead of silent.

type BaselineStatus added in v0.21.0

type BaselineStatus struct {
	// State: idle | running | succeeded | failed, plus two terminal states
	// only a sql-export build reaches once its staged files are gone —
	// downloaded (the archive reached a client) and expired (the download
	// deadline passed, or the files were removed from under it).
	State      string `json:"state"`
	Since      string `json:"since,omitempty"`
	FinishedAt string `json:"finished_at,omitempty"`
	LastError  string `json:"last_error,omitempty"`
	// Published: this run left a complete snapshot in the server's local
	// directory. Normally that is just State == "succeeded", but the two
	// diverge on one failure — the fold finished and marked the snapshot and
	// only the upload to the backup destination failed (#1539) — and that is
	// exactly the case where asking "did it fail?" gives the wrong answer to
	// "is a backup still owed?".
	Published bool `json:"published,omitempty"`
	// Uploading: the snapshot is published locally and its copy to the backup
	// destination is still in flight (#1725). The server's job slot is
	// already free; Uploaded is filled in when the copy completes.
	Uploading bool `json:"uploading,omitempty"`
	// Swept counts OTHER local snapshots a full backup sent to the destination
	// because the destination lacked them (a refresh whose upload failed).
	Swept  int `json:"swept,omitempty"`
	Tables int `json:"tables,omitempty"`
	// Carried counts tables published by reusing the previous snapshot's file
	// rather than folding them again (refresh and restore only). It is the
	// ONLY confirmation the operator gets that the reuse setting did anything:
	// without it a run that reused every file and a run that rewrote every
	// file report identically.
	Carried int `json:"carried,omitempty"`
	// CarriedCopied narrows Carried to the reuses that fell back to a full
	// byte copy (no hard link, so no disk saved). Without the split the UI
	// confirmed a disk saving the daemon log denied (#1578).
	CarriedCopied int   `json:"carried_copied,omitempty"`
	Rows          int64 `json:"rows,omitempty"`
	// Bytes is the finished artifact's on-disk weight (sql-export builds
	// only) — the UI's download confirm and Ready line read it.
	Bytes    int64 `json:"bytes,omitempty"`
	Uploaded int   `json:"uploaded,omitempty"`
	// At is the anchor instant of a refresh or restore run (RFC3339 UTC): the
	// moment the published snapshot represents, which is also its directory
	// name. A sql-export build stamps it too (the instant the dump
	// represents; it publishes no snapshot). Empty on dump jobs (the anchor
	// is chosen mid-run).
	At string `json:"at,omitempty"`
	// ExpiresAt (sql-export builds only, RFC3339 UTC) is when a finished
	// build is removed from the daemon's disk unless downloaded first; the
	// Backups page shows it as the download deadline.
	ExpiresAt string `json:"expires_at,omitempty"`
	// DownloadedAt (sql-export builds only, RFC3339 UTC) stamps the
	// download that consumed the build.
	DownloadedAt string `json:"downloaded_at,omitempty"`
	// StagingError (sql-export builds only) says why the staged files are
	// still on disk when they should not be (a removal that failed and is
	// retried every minute) or why they could not be read. Empty when fine.
	StagingError string `json:"staging_error,omitempty"`
	// RemovalOwed (sql-export builds only) is true while THIS build's own
	// removal has been decided and has not succeeded yet: its files are
	// still on disk but it is no longer downloadable. A StagingError about
	// a previous build (one this build's start could not clear) leaves it
	// false, and the build stays downloadable.
	RemovalOwed bool `json:"removal_owed,omitempty"`
	// Refused counts tables a refresh declined to fold (gap / schema change).
	// A refresh that refuses every table is not a failure of the daemon — it is
	// a correct fail-closed verdict — so it reports succeeded=false with this
	// count rather than an opaque error.
	Refused int `json:"refused,omitempty"`
	// TooManyChanges: the run refused for too many changed rows
	// (reconstruct.ErrTouchedRowBudget, #1107): one of a table's on-disk groups
	// of changes passed the per-table limit, or a table with no backup passed
	// the limit itself. The next automatic
	// refresh starts from the same backup over a longer window and would be
	// refused again, so the page asks for a full backup instead of promising
	// a retry.
	TooManyChanges bool `json:"too_many_changes,omitempty"`
	// DiskRefused: the run stopped for disk space (#1614), refused before a
	// file it could not hold, or a write that found the disk full. A scheduled update refused this way must NOT
	// fall back to a full backup: that one writes into the same directory.
	DiskRefused bool `json:"disk_refused,omitempty"`
}

type Config

type Config struct {
	// DB is the boot index database, from the command-line DSN. May be nil
	// when the console is registry-only (servers come from Registry) or in
	// unit tests that exercise only middleware/asset routes.
	DB *sql.DB
	// DBName is the boot index database name (from the DSN), used by the
	// query planner and status collection.
	DBName string
	// BootDSN is the boot entry's DSN, used ONLY to render the masked
	// host/user/dbname view in /api/servers. Optional.
	BootDSN string
	// HideBoot removes the boot entry from the UI entirely (selector, server
	// list, default selection). Set by source-less `bintrail-console watch`:
	// its boot index is only the control plane's anchor database — no stream
	// ever writes to it — and surfacing it as a "server" made fresh installs
	// look pre-populated. Header-less requests still resolve to the boot
	// bundle underneath, so the views render before the first server exists.
	HideBoot bool
	// Registry is the named-server store (a local YAML file — the only thing
	// the console ever writes). nil means an empty in-memory registry.
	Registry *Registry
	// Listen is the bind address (host:port). Default 127.0.0.1:8090.
	Listen string
	// Token is an OPT-IN static automation credential — set it explicitly for
	// scripts/curl. It is never generated. Password login is the primary path:
	// with no token and no password, a loopback bind (or AllowSetup) enters
	// first-run setup, and a non-loopback bind is refused.
	Token string
	// NoArchive disables Parquet archive auto-discovery on the boot entry.
	// The caller forces this true when a profile is active (archives do not
	// enforce RBAC). Registry entries carry their own per-server flag.
	NoArchive bool
	// DenyTables and RedactColumns are the resolved profile RBAC rules,
	// applied to every query the console runs — on EVERY server.
	DenyTables    []query.SchemaTable
	RedactColumns []query.SchemaTableColumn
	// ProfileActive is set by the caller whenever a profile NAME was supplied,
	// even if it resolved to ZERO deny/redact rules (an empty profile). It forces
	// query.Options.ProfileActive on every query so QueryText/QueryHash are
	// withheld under EVERY named profile (#699), and — since Parquet archives do
	// not run the redaction pass — couples with NoArchive so archive rows cannot
	// leak that statement text either (#838). Independent of the rule count,
	// which stays the signal for rbacActive() (cascade/reconstruct gating).
	ProfileActive bool
	// AllowedHosts is an optional allowlist of hostnames accepted in the Host
	// header (in addition to IP literals and localhost), for operators who
	// front the console with a DNS name.
	AllowedHosts []string
	// MonitorCtrl is the control-plane supervisor, wired in ONLY by
	// `bintrail-console watch` (the write-capable daemon). nil on the
	// standalone read-only console: /api/capabilities reports monitor:false
	// there and every monitor verb refuses at the endpoint with 403,
	// mirroring how reconstruct gates on baselineConfigured.
	MonitorCtrl MonitorController
	// BaselineCtrl runs in-process baseline snapshots (dump→convert→upload) for
	// a monitored server (#613). Wired in ONLY by `bintrail-console watch` when
	// the operator opts in (BINTRAIL_CONSOLE_BASELINE_TRIGGER=1); nil otherwise,
	// where the trigger endpoint refuses with 403 and /api/capabilities reports
	// baseline_trigger:false. Set together with MonitorCtrl (both control-plane).
	BaselineCtrl BaselineController
	// BaselineRefresh reports the daemon's PERIODIC baseline refresh (#1171).
	// Wired in ONLY by `bintrail-console watch --baseline-refresh-interval`, and
	// deliberately a separate field from BaselineCtrl because the two features
	// are independently opt-in — the refresh needs no mydumper and no
	// BINTRAIL_CONSOLE_BASELINE_TRIGGER=1. nil when no refresh loop runs, and the
	// baseline listing then simply carries no refresh section.
	BaselineRefresh BaselineRefreshReporter
	// BaselineRestore runs operator-chosen point-in-time restores into the
	// server's own baseline store. Wired whenever the watch daemon has a
	// baseline supervisor (creation or refresh opt-in); nil elsewhere.
	BaselineRestore BaselineRestorer
	// BackupSchedules is the watch daemon's per-server backup schedule loop
	// (#1442). Wired whenever the daemon has a baseline supervisor; nil
	// elsewhere, where the schedule endpoints refuse with 403 and the listing
	// reports a saved schedule as not runnable.
	BackupSchedules BackupScheduleReporter
	// SQLExport builds custom .sql backups (fold at a chosen instant,
	// download as a mydumper-format dump). Wired with BaselineRestore.
	SQLExport SQLExporter
	// BaselineHistory is the persisted record of baseline runs this daemon
	// performed (dump/refresh/restore), used by the backups detail surface to
	// report exact durations. Nil = no history (exact durations unavailable).
	BaselineHistory *BaselineRunHistory
	// SchemaSnapshotCtrl re-reads a monitored source's column layout and
	// restarts that server's stream onto the result (#1296). Wired in by
	// `bintrail-console watch` alongside MonitorCtrl and needing no separate
	// opt-in — unlike BaselineCtrl it starts no dump and copies no row data.
	// nil elsewhere, where the endpoints refuse with 403 and
	// /api/capabilities reports schema_snapshot_trigger:false.
	SchemaSnapshotCtrl SchemaSnapshotController
	// VerifyCtrl runs bintrail verify's engine in-process for a monitored
	// server (#677). Wired in ONLY by `bintrail-console watch` when the
	// operator opts in (BINTRAIL_CONSOLE_VERIFY_TRIGGER=1); nil otherwise,
	// where the trigger endpoint refuses with 403 and /api/capabilities reports
	// verify_trigger:false. Set together with MonitorCtrl (both control-plane).
	VerifyCtrl VerifyController
	// VerifyHistory is the persisted verify-run history store (#1191), wired
	// in by `bintrail-console watch` together with VerifyCtrl. nil elsewhere;
	// the history endpoint then refuses with 403 like the other verify verbs.
	VerifyHistory *VerifyHistory
	// Telemetry is the live usage-telemetry client, wired in by
	// `bintrail-console watch` so the UI opt-out toggle stops the running
	// daemon's beacons immediately. nil on the read-only console (serve), where
	// the toggle still persists the machine-wide choice to the consent file.
	Telemetry TelemetryController
	// BaselineDir / BaselineS3 enable point-in-time reconstruct (Phase 2) on
	// the boot entry. When either is set (and no RBAC profile is active), the
	// "Reconstruct" surface is exposed. BaselineDir takes precedence;
	// BaselineS3 is an s3:// prefix. Registry entries carry their own.
	BaselineDir string
	BaselineS3  string
	// AuthPath locates the console credential file (username + bcrypt hash,
	// written by `bintrail-console user set-password`). Empty means
	// DefaultAuthPath(). A missing file means password login is not
	// configured; a corrupt one fails New loudly.
	AuthPath string
	// MCPTokenPath locates the managed MCP token file (SHA-256 only, written
	// by the Settings → Connect AI generate flow — #1052). Empty means
	// DefaultMCPTokenPath(). A missing file means no managed token; a corrupt
	// one is logged loudly and disables the managed token until regenerated —
	// unlike AuthPath, it deliberately never fails New (the daemon may be the
	// stream supervisor).
	MCPTokenPath string
	// TLSCert / TLSKey serve the console over HTTPS (both-or-neither). Static
	// files only — rotation is a restart; ACME is out of scope.
	TLSCert string
	TLSKey  string
	// AllowSetup permits browser first-run password setup on a NON-loopback
	// bind — an assertion that the bind is access-controlled by other means.
	// The compose stack sets it because it binds 0.0.0.0 inside the container
	// but publishes the port on the host's loopback only; the container can't
	// see that host mapping, so the operator asserts it. Loopback binds always
	// allow setup regardless. Off by default: an unguarded setup endpoint on a
	// truly public bind would let the first stranger claim the password.
	AllowSetup bool
	// RotationDefaults carries the daemon's --rotate-* flag/env values so
	// GET /api/rotation can report the effective policy (and the console panel
	// prefill it) before any console override is saved. Set by the watch
	// daemon; zero on the standalone serve (which runs no rotation loop and
	// hides the panel).
	RotationDefaults RotationDefaults

	// BaselineRefreshTargets reports, LIVE, how many servers the next refresh
	// tick will cover and how many were skipped for keeping their baselines
	// only in S3. Wired by the watch daemon only when the refresh loop is
	// scheduled; nil everywhere else, and the DTO then omits the counts. Live
	// rather than a boot snapshot because the loop recomputes its target set
	// every tick — a snapshot would go stale the moment a server is added,
	// which is exactly the fresh-install shape #1579 is about (enabled and
	// scheduled both true, zero servers refreshable, and the page silent).
	BaselineRefreshTargets func() (targets, skippedS3Only int)
	// BaselineRefreshDefaults carries the daemon's baseline-refresh flag/env
	// values, the fallback the settings panel reports when no console override
	// is saved. Same role as RotationDefaults above.
	BaselineRefreshDefaults BaselineRefreshDefaults

	// BackupSettingsDefaults carries the daemon-wide backup/snapshot flag and
	// env values the Backup settings page (#1582) reports
	// read-only: what the process was told, verbatim, with the exact name to
	// change each one under. Same role as RotationDefaults above.
	BackupSettingsDefaults BackupSettingsDefaults
	// Version is the running build's version string ("0.36.0", or "dev" on
	// unversioned builds), reported in /api/capabilities so the frontend can
	// link release artifacts (the .mcpb bundle) matching the running binary.
	// Optional; empty reads as an unversioned build.
	Version string
	// FlashbackListen is the address the embedded MySQL-protocol time-travel
	// port (#996) is bound on, reported by GET /api/flashback so the Connect
	// page can show how to reach it (#1446). Set ONLY by `bintrail-console
	// watch --flashback-listen`, which refuses to start when that bind fails,
	// so a non-empty value is an address that was bound at startup. It is
	// NOT a liveness signal: a mid-run serve failure on that port is logged
	// and swallowed (startFlashbackPort), and nothing here re-probes it.
	// Empty on the standalone serve (which owns no such port) and on a watch
	// that did not opt in; the page then reports the port as off. Display
	// only: the console never opens or closes the port itself.
	FlashbackListen string
}

Config configures a console Server. The caller (cmd/bintrail/console.go) is responsible for connecting the boot DB (from --index-dsn), running EnsureSchema on it, and resolving the profile's RBAC rules before constructing the server. Registry servers are connected lazily by the console itself — and never schema-migrated (see connManager).

type DoctorCheck

type DoctorCheck struct {
	Name        string `json:"name"`
	Status      string `json:"status"`
	Detail      string `json:"detail,omitempty"`
	Remediation string `json:"remediation,omitempty"`
}

DoctorCheck is one preflight check result, JSON-shaped for the UI's remediation cards. Status is pass|fail|warn|skip.

type DoctorReport

type DoctorReport struct {
	Checks   []DoctorCheck `json:"checks"`
	Passed   int           `json:"passed"`
	Failed   int           `json:"failed"`
	Warnings int           `json:"warnings"`
	Skipped  int           `json:"skipped"`
}

DoctorReport aggregates the preflight checks for one source.

type FirstRunReport added in v0.82.0

type FirstRunReport struct {
	Complete   bool           `json:"complete"`
	Steps      []FirstRunStep `json:"steps"`
	CheckError string         `json:"check_error,omitempty"`
}

FirstRunReport is GET /api/servers/{id}/first-run. Complete: the server has indexed a change, and the list is no longer shown.

type FirstRunStep added in v0.82.0

type FirstRunStep struct {
	Name   string `json:"name"`
	State  string `json:"state"`
	Detail string `json:"detail,omitempty"`
	Fix    string `json:"fix,omitempty"`
}

FirstRunStep is one row of the first-run list.

type FlashbackTarget added in v0.31.0

type FlashbackTarget struct {
	// IndexDB is the open connection to the server's per-source index. It is
	// owned by the connManager — the serving layer must not Close it.
	IndexDB *sql.DB
	// IndexDBName is the schema where binlog_events lives (the query planner
	// scopes information_schema.PARTITIONS to it).
	IndexDBName string
	// BaselineDir / BaselineS3 are the resolved _snapshot baseline source, split
	// by scheme (dir-preferred, mirroring the console's Time-travel tab). The
	// #766 local→S3 fallback the console bundle also carries is intentionally
	// dropped — a documented single-source-parity edge for the embedded port
	// (see docs/time-travel-sql.md).
	BaselineDir string
	BaselineS3  string
	// NoArchive disables archive auto-discovery for this server.
	NoArchive bool
	// DefaultSchema is the source database name (from the registry SourceDSN),
	// seeded so USE-less `_flashback.<table>` queries resolve; empty for the
	// boot entry (no registry SourceDSN).
	DefaultSchema string
}

FlashbackTarget is the go-mysql-free resolution of a flashback connection's target server (issue #996): the per-source index handle, baseline source, and default schema the MySQL-protocol serving layer needs to build a shim handler.

It lives here — but the serving code does NOT — because internal/console is a read-layer package barred from linking go-mysql (#528, TestReadLayerDoesNotLinkGoMySQL). cmd/bintrail-console owns the protocol server and consumes this plain struct; nothing here imports the protocol or capture libraries.

type MCPTokenFile added in v0.40.0

type MCPTokenFile struct {
	Version     int    `yaml:"version"`
	TokenSHA256 string `yaml:"token_sha256"`
	CreatedAt   string `yaml:"created_at"`
	// Permissions is the grant set recorded at mint time (#1124): the minting
	// session's permissions, so a token can never exceed what its minter could
	// do through the browser API. A nil (absent) field means the FULL read
	// surface — both the file a full-access minter writes (every OSS session,
	// the static token, a password login) and every file minted before grants
	// were recorded, which were all minted by full-access sessions in OSS
	// builds. A present-but-empty list grants nothing; the pointer keeps
	// "absent" and "empty" distinguishable across the YAML round trip.
	Permissions *[]string `yaml:"permissions,omitempty"`
	// Extra preserves unknown top-level fields a newer binary wrote, across
	// this binary's load→save cycle.
	Extra map[string]any `yaml:",inline"`
	// contains filtered or unexported fields
}

MCPTokenFile is the on-disk managed MCP token: only the SHA-256 of the token is persisted (API-key pattern — the plaintext is shown once at generation and never stored), so a read of the file does not yield a usable credential. The versioned envelope and Extra leave room for additive fields without a format break (authfile.go precedent). Construct only via LoadMCPTokenFile / GenerateMCPToken — readOnly is set at load time.

func GenerateMCPToken added in v0.40.0

func GenerateMCPToken(path string, grants []ext.Permission) (string, *MCPTokenFile, error)

GenerateMCPToken mints a new managed token, persists its SHA-256 at path (atomic, 0600), and returns the plaintext and the stored record. A pre-existing file is replaced (rotate); a MALFORMED file is replaced too (the UI's Generate button is the documented self-heal); an unreadable file or a newer-version file refuses — what could not be inspected must not be destroyed.

grants is the permission set the token will carry on /mcp tool dispatch (#1124): nil records no permissions field — the full read surface, the full-access-minter case — while a non-nil slice (even empty) is recorded verbatim and caps the token at exactly those permissions. A rotate always re-records from the CURRENT minter, never from the replaced file.

func LoadMCPTokenFile added in v0.40.0

func LoadMCPTokenFile(path string) (*MCPTokenFile, error)

LoadMCPTokenFile reads the managed-token file at path. A missing file returns (nil, nil): no managed token configured. An unparseable file, or a malformed digest in a file THIS version owns, errors with errMCPTokenFileMalformed. A NEWER-version file is never an error for its payload: it loads read-only, and a digest this binary can't read simply never authenticates — a newer format must degrade, not brick startup or demand deletion (the readOnly contract).

func (*MCPTokenFile) ReadOnly added in v0.40.0

func (f *MCPTokenFile) ReadOnly() bool

ReadOnly reports whether write paths must refuse (newer-version file).

type MonitorController

type MonitorController interface {
	// DeriveIndexDSN returns the index DSN a monitored entry should use — a
	// dedicated per-source database on the daemon's index MySQL server. It
	// does not create anything; Start does.
	DeriveIndexDSN(entryID string) (string, error)
	// Doctor runs the preflight checks against the entry's source (and its
	// index DSN, which may not exist yet — that is a pass, init creates it).
	Doctor(ctx context.Context, e ServerEntry) (*DoctorReport, error)
	// Start provisions the entry's index database (CREATE DATABASE + tables +
	// schema migration — the supervisor is a WRITER, the same role the cmd
	// layer plays for the boot DSN, so the console's never-migrates invariant
	// holds) and launches the supervised stream. Idempotent for an already
	// running entry. ctx bounds only the synchronous provisioning; the stream
	// itself lives on the daemon's lifecycle.
	Start(ctx context.Context, e ServerEntry) error
	// Stop cancels the entry's stream and releases its advisory lock.
	// Idempotent for an already stopped entry.
	Stop(ctx context.Context, entryID string) error
	// Status reports the entry's current monitor state.
	Status(entryID string) MonitorStatus
}

MonitorController is the control-plane supervisor as the console sees it. All methods must be safe for concurrent use. Errors returned to handlers are written into HTTP responses — implementations must pre-scrub DSN secrets out of them.

type MonitorStatus

type MonitorStatus struct {
	// State: "stopped" | "pending" | "running" | "stalled" | "lost_position"
	// | "failed".
	//   - "pending" covers launch through the stream's first checkpoint —
	//     connecting, snapshotting, discovering the start position. Only a
	//     saved checkpoint (or indexed batch) flips it to "running" (#407).
	//   - "stalled" and "lost_position" are running variants derived at read
	//     time: the stream goroutine is alive, but it has made no progress
	//     for several minutes (stalled) or an unfillable binlog gap forced
	//     it to skip permanently lost events (lost_position — durable,
	//     persisted in stream_state, re-hydrated on Start, cleared only by
	//     an explicit Stop).
	//   - "failed" carries LastError; the supervisor keeps retrying with
	//     backoff — "unhealthy, recovering", not terminal — unless the
	//     circuit breaker gave up after hours of continuous crash-looping
	//     (the message says so; Start re-arms it).
	State     string `json:"state"`
	LastError string `json:"last_error,omitempty"`
	// Since is when the underlying STORED state was entered (RFC3339), empty
	// for stopped. For the derived stalled/lost_position presentations it
	// still reflects the running transition, not when the stream stalled or
	// lost its position.
	Since string `json:"since,omitempty"`
	// SourceConnected: the latest run's stream opened the source connection
	// (#1606). It resets when a run starts, and keeps that run's value while
	// the run waits to retry or after it stops.
	SourceConnected bool `json:"source_connected,omitempty"`
	// Retrying: a failed state the supervisor will retry on its own after a
	// backoff. False for a failure it gave up on and for a Start that failed
	// while setting up, which both wait for Start (#1606).
	Retrying bool `json:"retrying,omitempty"`
	// Phase names a long startup step the stream is inside right now, so
	// "pending" can say WHICH part of starting up it is stuck on (#1690).
	// Currently only "resume_cleanup": the pre-capture delete of events a
	// replayed window would re-index, minutes of work on a large index.
	// Empty whenever no such step is running — including between retries and
	// for a stream that never reached one.
	Phase string `json:"phase,omitempty"`
}

MonitorStatus is the supervisor's view of one entry's stream.

type ParsedBackupSchedule added in v0.71.0

type ParsedBackupSchedule struct {
	Every time.Duration
	At    time.Duration // offset from midnight UTC
}

ParsedBackupSchedule is a validated schedule, ready for slot arithmetic.

func (ParsedBackupSchedule) BackupsPer30Days added in v0.71.0

func (p ParsedBackupSchedule) BackupsPer30Days() int64

BackupsPer30Days is how many backups this schedule publishes in 30 days: every one a full-table snapshot, and on a server without an S3 destination none of them ever removed automatically.

func (ParsedBackupSchedule) NextRun added in v0.71.0

func (p ParsedBackupSchedule) NextRun(now time.Time) time.Time

NextRun returns the first slot strictly after now.

func (ParsedBackupSchedule) SlotAtOrBefore added in v0.71.0

func (p ParsedBackupSchedule) SlotAtOrBefore(now time.Time) time.Time

SlotAtOrBefore returns the latest slot at or before now.

type Registry

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

Registry is the console's named-server store: a local YAML file, the ONLY thing the console ever writes. All mutations rewrite the file atomically (temp file + fsync + rename) under a mutex — this is bintrail's first programmatically-mutated config file, so a plain os.WriteFile (which can interleave concurrent writers) is not enough.

func LoadRegistry

func LoadRegistry(path string) (*Registry, error)

LoadRegistry reads the registry file at path. A missing or empty file is an empty registry, not an error. The parse is deliberately NON-strict (unlike shim.yaml's UnmarshalStrict): an older binary must tolerate top-level fields a newer one added. path == "" creates an in-memory registry that never touches disk — for unit tests and callers without persistence.

func (*Registry) Add

func (r *Registry) Add(e ServerEntry) (ServerEntry, error)

Add validates, mints an id, appends, and persists the entry. The returned entry carries the generated ID.

func (*Registry) BaselineRefresh added in v0.70.0

func (r *Registry) BaselineRefresh() (BaselineRefreshConfig, bool)

BaselineRefresh returns the saved override and whether one exists. Absent means the daemon's own defaults are in force.

func (*Registry) Delete

func (r *Registry) Delete(id string) error

Delete removes the entry with the given id and persists.

func (*Registry) Get

func (r *Registry) Get(id string) (ServerEntry, bool)

Get returns the entry with the given id. Like List, the entry's Extra map is shared with the registry — treat it as read-only.

func (*Registry) Len

func (r *Registry) Len() int

Len reports the number of registry entries.

func (*Registry) List

func (r *Registry) List() []ServerEntry

List returns a copy of the entries, in file order. The copy is shallow: each entry's Extra map is shared with the registry — treat returned entries as read-only (nothing in the console mutates Extra off a copy today).

func (*Registry) ReadOnly

func (r *Registry) ReadOnly() bool

ReadOnly reports whether mutating operations are refused (newer-version file).

func (*Registry) Rotation added in v0.13.0

func (r *Registry) Rotation() (RotationConfig, bool)

Rotation returns the saved global rotation policy, or false when none is set (the daemon's --rotate-* flags/env are in force). The rotation loop's settings provider reads this every cycle, so an override applies live.

func (*Registry) SetBaselineRefresh added in v0.70.0

func (r *Registry) SetBaselineRefresh(bc *BaselineRefreshConfig) error

SetBaselineRefresh persists a global baseline-refresh override, rolling back the in-memory value if the write fails so the two never diverge.

A nil argument CLEARS the override, which is what returns the daemon's own flag and environment to force. Without it the panel would be a one-way door: the tri-state that lets a saved "off" beat a flag saying "on" also means that once anything is saved, the flag can never be heard again short of editing the file by hand.

func (*Registry) SetProcessS3Location added in v0.82.0

func (r *Registry) SetProcessS3Location(label, loc string)

SetProcessS3Location registers an S3 location the daemon reads for servers with none of their own: the --baseline-s3 fallback (withBaselineDefaults) that every server with no Backups location inherits, and that the boot entry reads. Its bucket counts as a bucket with no store: a server naming it with a store is refused, and a hand-edited one leaves it unrouted. label names the setting in refusals and warnings. Called before serving.

func (*Registry) SetRotation added in v0.13.0

func (r *Registry) SetRotation(rc RotationConfig) error

SetRotation persists the global rotation policy. Like every registry mutation it rewrites the file atomically and is refused on a newer-version (read-only) file. The caller validates the field grammar.

func (*Registry) Update

func (r *Registry) Update(e ServerEntry) error

Update replaces the entry with e.ID and persists. The caller is responsible for keep-password merging — the registry stores exactly what it is given. Unknown phase-2 fields survive: the stored entry's Extra is carried over unless the caller supplied its own.

type RotationConfig added in v0.13.0

type RotationConfig struct {
	Retain    string `yaml:"retain"`
	Interval  string `yaml:"interval"`
	AddFuture int    `yaml:"add_future"`
}

RotationConfig is the daemon-global built-in-rotation policy, editable from the console UI. It is stored once in the registry envelope, NOT per-server: the rotation loop is a single shared ticker, so retain/interval/add-future necessarily apply to every index the daemon rotates. Absent (nil) = the daemon's --rotate-* flags / BINTRAIL_ROTATE_* env stay in force. The fields hold the operator-typed strings (e.g. "30d", "1h") so they round-trip exactly and the engine parses them with the same grammar as the flags.

type RotationDefaults added in v0.13.0

type RotationDefaults struct {
	Retain    string
	Interval  string
	AddFuture int
	// Enabled is false when the daemon was started with rotation off
	// (--rotate-retain off). The loop is not running then, so a console-saved
	// override would need a restart — the panel warns in that state.
	Enabled bool
}

RotationDefaults is the daemon-side built-in-rotation policy, surfaced to the console read-only as the fallback shown when no override is saved.

type SQLExportRequest added in v0.66.0

type SQLExportRequest struct {
	ServerID    string
	ServerName  string
	IndexDSN    string
	BaselineSrc string // local directory or s3:// prefix
	At          time.Time
}

SQLExportRequest identifies the server and the instant to build for.

type SQLExportStagedBuild added in v0.71.0

type SQLExportStagedBuild struct {
	ServerID  string
	State     string // running | succeeded | failed (only while its removal is still owed) | replaced (a previous build a newer one could not remove)
	At        string // the instant the dump represents (RFC3339 UTC)
	ExpiresAt string // when a finished build is removed unless downloaded first
	// Bytes is the live on-disk size; meaningful only when BytesKnown. A
	// walk that failed part-way reports unknown, never the fraction it
	// managed to count.
	Bytes      int64
	BytesKnown bool
	// StagingError is why the build is still on disk when it should not be
	// (a removal that failed) or why it cannot be read; empty when fine.
	StagingError string
}

SQLExportStagedBuild is one build on disk: in progress, finished and waiting for its download, or owed a removal that has not succeeded yet.

type SQLExportStagingInfo added in v0.71.0

type SQLExportStagingInfo struct {
	Dir    string                 // the staging base every build lives under
	TTL    time.Duration          // how long a finished build stays downloadable
	Builds []SQLExportStagedBuild // sorted by server id
}

SQLExportStagingInfo is what the sql-export staging holds right now.

type SQLExporter added in v0.66.0

type SQLExporter interface {
	// TriggerSQLExport starts the build asynchronously. ErrBaselineRunning
	// when another baseline job for the server is in flight.
	TriggerSQLExport(req SQLExportRequest) error
	// SQLExportStatus reports the last build for a server (idle if none).
	SQLExportStatus(serverID string) BaselineStatus
	// SQLExportDir returns the finished dump's directory and the status it
	// belongs to, from one locked snapshot (so the caller can never label
	// one build's bytes with another build's instant); ok is false until a
	// build has succeeded and its directory affirmatively carries the
	// completeness marker.
	SQLExportDir(serverID string) (dir string, status BaselineStatus, ok bool)
	// SQLExportHold registers a download of the build in dir so the
	// exporter neither expires nor removes it under the stream; release
	// drops the hold (idempotent). ok is false when dir is no longer the
	// server's finished build, in which case the caller answers not-ready.
	SQLExportHold(serverID, dir string) (release func(), ok bool)
	// SQLExportDelivered tells the exporter the whole archive built in dir
	// was written to the client, so the staged copy can go now rather than
	// at its download deadline. dir pins which build: a newer build that
	// replaced it mid-stream must not be removed on the strength of the old
	// one's download.
	SQLExportDelivered(serverID, dir string)
	// SQLExportStaged reports every build currently on disk (running or
	// waiting for its download) with its live size, for the Storage page.
	SQLExportStaged() SQLExportStagingInfo
}

SQLExporter builds a custom .sql backup: fold the backup at-or-before an operator-chosen instant forward through the index and write it as a mydumper-format dump the operator downloads and loads with myloader (or the mysql client, applying mydumper's session assumptions), no bintrail needed on the restore side. A separate interface from the other baseline controllers so its wiring CAN diverge (the #1171 lesson); today it deliberately rides the same supervisor as BaselineRestore — see wireBaselineExtras for why that derivation is safe here.

type ScheduleRefusal added in v0.71.0

type ScheduleRefusal struct{ Reason string }

ScheduleRefusal is CheckBackupSchedule's error: the class plus the reason.

func (*ScheduleRefusal) Error added in v0.71.0

func (r *ScheduleRefusal) Error() string

func (*ScheduleRefusal) Is added in v0.71.0

func (r *ScheduleRefusal) Is(target error) bool

Is makes errors.Is(err, ErrBackupScheduleNotRunnable) true.

type SchemaSnapshotController added in v0.51.0

type SchemaSnapshotController interface {
	// Trigger takes a fresh snapshot in the background and returns immediately.
	// Returns ErrSchemaSnapshotRunning if one is already running for
	// req.ServerID.
	Trigger(req SchemaSnapshotRequest) error
	// Status reports the latest known state for a server (idle if none has run
	// in this process).
	Status(serverID string) SchemaSnapshotStatus
}

SchemaSnapshotController re-reads a monitored source's column layout into the index and puts the running capture stream onto it (#1296).

A SCHEMA SNAPSHOT IS NOT A BASELINE, and this file is where the two are kept apart. A schema snapshot is the record of each table's columns that capture decodes row events against — cheap, metadata-only, read from information_schema. A baseline (BaselineController) is a full COPY of the data, produced by mydumper. The console offers both, and the capture-degraded banner used to say "take a fresh snapshot" next to a button labelled "Create baseline": an operator who follows that runs a dump they did not need while capture stays broken. Keep the two vocabularies separate in every type name, endpoint and label here.

Wired in only by `bintrail-console watch`, which owns the supervised streams; nil on the standalone read-only console, where the endpoints refuse with 403 (mirroring how MonitorController gates the monitor verbs).

type SchemaSnapshotRequest added in v0.51.0

type SchemaSnapshotRequest struct {
	ServerID   string
	ServerName string
	SourceDSN  string
	// IndexDSN is the entry's per-source index database — where the new
	// snapshot rows are written and where the restarted stream will read them
	// from. The two must be the same database or the stream would reload the
	// snapshot it already had.
	IndexDSN string
	Schemas  []string
}

SchemaSnapshotRequest is the in-process job description the endpoint hands the controller. Both DSNs are secrets: they stay inside the process and are never written to disk or serialized into any HTTP response.

type SchemaSnapshotStatus added in v0.51.0

type SchemaSnapshotStatus struct {
	State      string `json:"state"` // idle | running | succeeded | failed
	Since      string `json:"since,omitempty"`
	FinishedAt string `json:"finished_at,omitempty"`
	LastError  string `json:"last_error,omitempty"`
	SnapshotID int    `json:"snapshot_id,omitempty"`
	Tables     int    `json:"tables,omitempty"`
	// ExcludedTables names tables snapshot validation left out (no explicit
	// primary key / non-InnoDB). Reported because those tables are exactly the
	// ones that will KEEP being skipped after this run — an operator who
	// pressed the button to fix capture must not read "succeeded" as "every
	// table is captured now".
	ExcludedTables []string `json:"excluded_tables,omitempty"`
	// StreamReloaded reports whether the capture stream was actually restarted
	// onto the new snapshot. A running stream holds its resolver in memory and
	// only swaps it on a DDL event, so a snapshot written underneath it changes
	// NOTHING until the stream reloads. Without this field a silent no-op would
	// be indistinguishable from a fix — the precise failure this feature exists
	// to prevent.
	StreamReloaded bool `json:"stream_reloaded"`
	// ReloadError explains a failed reload. The snapshot itself is still
	// durable, so this is not a job failure — it is "the new snapshot is
	// recorded but capture is still running on the old one; restart it".
	ReloadError string `json:"reload_error,omitempty"`
}

SchemaSnapshotStatus is the pollable state of a server's most recent snapshot job.

type Server

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

Server is a configured, ready-to-run console HTTP server. It holds only process-global state; everything per-server (db, engine, resolver, baseline gates) lives in a connManager bundle resolved per request from the X-Bintrail-Server header.

func New

func New(cfg Config) (*Server, error)

New validates the config, seeds the boot connection bundle, and assembles the middleware/route tree. It does no network I/O — call Run to listen.

func (*Server) Handler

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

Handler returns the fully assembled HTTP handler. Exposed for tests.

func (*Server) Listen

func (s *Server) Listen() (net.Listener, error)

Listen binds the configured address and returns the listener synchronously, so a caller can fail fast on a port conflict before reporting the server as ready (the standalone command blocks on Run, but `bintrail-console watch` starts the server in a goroutine and must surface a bind error up front).

func (*Server) NeedsSetup added in v0.12.0

func (s *Server) NeedsSetup() bool

NeedsSetup reports whether the console is in first-run setup: no credential at all, on a loopback bind (or a non-loopback bind the operator marked access-controlled via --allow-setup), so the operator must create a password in the browser (via the unauthenticated /api/auth/setup endpoint). The cmd layer uses it to print "create your password" instead of a URL with a token.

func (*Server) PasswordLogin added in v0.11.0

func (s *Server) PasswordLogin() bool

PasswordLogin reports whether a console password was configured at boot — the cmd layer keys its banner wording on it.

func (*Server) ResolveFlashback added in v0.31.0

func (s *Server) ResolveFlashback(ctx context.Context, selector string) (FlashbackTarget, error)

ResolveFlashback maps a flashback connection username to its target server's per-source index + baseline, opening the connection lazily via connManager. The username selects the server by registry ID, display Name, or "default" (the boot entry). Returns ErrUnknownServer when the selector matches no selectable server — the serving layer turns that into a MySQL "no such database" error on the client's first query. The registry is read live, so servers added in the console mid-session are reachable without a restart.

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run binds and serves, blocking until ctx is cancelled, then shuts down gracefully. Returns the bind/serve error, or nil on a clean shutdown.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context, ln net.Listener) error

Serve serves on ln and blocks until ctx is cancelled, then shuts the server down gracefully (5s drain). It takes ownership of ln (Shutdown closes it). Lazily-opened registry connections are closed on the way out; the boot entry's db belongs to the cmd layer and its deferred Close.

func (*Server) Token

func (s *Server) Token() string

Token returns the static automation token — never the UI-managed MCP token (the flashback port depends on that distinction). Empty in password-only mode.

func (*Server) URL

func (s *Server) URL() string

URL returns the bootstrap URL the operator opens in a browser. Token mode includes the jupyter-style ?token=; password mode omits it (printing a live credential into logs and shell history serves no one when the login form is the entry point). The scheme follows TLS.

type ServerEntry

type ServerEntry struct {
	// ID is a server-generated stable 8-byte hex key; it survives renames and
	// is what the browser sends in the X-Bintrail-Server header.
	ID string `yaml:"id"`
	// Name is the mutable operator-facing label, unique across the registry.
	Name string `yaml:"name"`
	// DSN is the full index DSN, password included — a secret at rest (the
	// file is 0600, same class as shim.yaml's mysql_password).
	DSN         string `yaml:"index_dsn"`
	BaselineDir string `yaml:"baseline_dir,omitempty"`
	BaselineS3  string `yaml:"baseline_s3,omitempty"`
	NoArchive   bool   `yaml:"no_archive,omitempty"`

	// SourceDSN is the source MySQL to monitor — replication credentials,
	// a secret exactly like DSN, never serialized to any HTTP response.
	// Empty = a view-only entry (no monitoring configured).
	SourceDSN string `yaml:"source_dsn,omitempty"`
	// SourceServerID overrides the auto-derived replica server id (0 = derive
	// from the source DSN, the same rule as `bintrail up`).
	SourceServerID uint32 `yaml:"source_server_id,omitempty"`
	// Schemas is the optional comma-separated schema filter for monitoring.
	Schemas string `yaml:"schemas,omitempty"`

	// ── Source family (#1019) ──
	// Flavor selects the capture engine: "" or "mysql" (MySQL), "mariadb"
	// (MySQL DSN, MariaDB GTID at stream), "postgres" (pgstreamrun). Empty →
	// mysql keeps every pre-#1019 entry working (mirrors an empty SSLMode).
	// This is the generic "which source family" field #623 (MariaDB) also
	// needs — one implementation, not two. Accessed via SourceFlavor().
	Flavor string `yaml:"flavor,omitempty"`
	// SourceSlot / SourcePublication configure PostgreSQL logical replication
	// (postgres flavor only): the operator-created publication and the
	// replication slot the capturer streams from. Empty for MySQL/MariaDB.
	SourceSlot        string `yaml:"source_slot,omitempty"`
	SourcePublication string `yaml:"source_publication,omitempty"`

	// ── Source TLS (#879) ──
	// SSLMode/SSLCA/SSLCert/SSLKey configure the TLS the supervisor uses when
	// connecting to the SOURCE MySQL. Empty SSLMode = the daemon default
	// ("preferred"), preserving pre-#879 behavior; SSLCA/SSLCert/SSLKey are file
	// paths on the daemon host (verify-ca / mutual TLS), not secrets like the
	// DSNs. On binaries that predate these they round-trip untouched via Extra.
	SSLMode string `yaml:"ssl_mode,omitempty"`
	SSLCA   string `yaml:"ssl_ca,omitempty"`
	SSLCert string `yaml:"ssl_cert,omitempty"`
	SSLKey  string `yaml:"ssl_key,omitempty"`
	// ArchiveS3 is the S3 destination (s3://bucket/prefix/) the daemon's
	// built-in rotation uploads this source's rotated Parquet partitions to
	// BEFORE dropping them — so the forensic record survives retention and
	// stays queryable (the console auto-discovers it). Empty = drop-only.
	// Region/credentials come from the ambient AWS chain (env / ~/.aws / IAM
	// role); a local staging dir is used transiently. Non-secret (a bucket
	// URL): unlike DSNs it is serialized to the masked HTTP responses.
	ArchiveS3 string `yaml:"archive_s3,omitempty"`
	// ── S3 store (#1575) ──
	// S3Endpoint / S3PathStyle / S3Region say where this server's S3 buckets
	// (ArchiveS3 and BaselineS3) live when that is not AWS or the process-wide
	// BINTRAIL_S3_ENDPOINT: an S3-compatible store (MinIO, Wasabi), a pinned
	// region, or both. Applied PER BUCKET at runtime (storage.BucketStore):
	// every upload to and every DuckDB read of a bucket named here goes to
	// this store, whichever server asked. Two servers naming the same bucket
	// with different stores is refused (ErrS3StoreConflict). S3PathStyle is
	// "", "path" or "vhost"; "" means path style, what MinIO needs. Locations,
	// not secrets: serialized to the masked DTO.
	S3Endpoint  string `yaml:"s3_endpoint,omitempty"`
	S3PathStyle string `yaml:"s3_path_style,omitempty"`
	S3Region    string `yaml:"s3_region,omitempty"`
	// S3AccessKeyID / S3SecretAccessKey sign requests for those buckets
	// instead of the ambient credential chain. Both or neither. They belong
	// to the bucket like the endpoint: two servers on one bucket must carry
	// the same pair. The secret is at rest here like the DSN passwords (file
	// 0600) and never leaves the process: the DTO carries the access key and
	// has_s3_secret_access_key only.
	S3AccessKeyID     string `yaml:"s3_access_key_id,omitempty"`
	S3SecretAccessKey string `yaml:"s3_secret_access_key,omitempty"`
	// MonitorDesired records the operator's intent to monitor this source.
	// The supervisor reconciles running streams against it at boot and on
	// every edit; nothing reads it until phase 3.
	MonitorDesired bool `yaml:"monitor_desired,omitempty"`
	// BackupSchedule is this server's unattended backup timer (#1442): a full
	// backup from the source, or a rebuild from the change history, on a
	// fixed grid. Nil = no schedule. Non-secret. Edited only through the
	// schedule endpoints; the server edit form carries it over untouched.
	// On binaries that predate it, it round-trips through Extra.
	BackupSchedule *BackupSchedule `yaml:"backup_schedule,omitempty"`

	// Extra is the forward-compat catch-all: unknown fields written by a NEWER
	// bintrail (e.g. the phase-2 control plane's source_dsn / server_id /
	// monitor_state) land here on load and re-emit verbatim on save, so an
	// older binary editing the file never drops them. yaml:",inline" requires
	// map[string]any specifically; non-strict Unmarshal alone would only
	// tolerate unknown fields on read — a re-marshal would lose them.
	Extra map[string]any `yaml:",inline"`
}

ServerEntry is one named index connection in the registry. DSN holds the full secret (including the password) and is NEVER serialized into an HTTP response — see serverDTO in servers_api.go for the masked wire view.

func (ServerEntry) BucketStore added in v0.82.0

func (e ServerEntry) BucketStore() (storage.BucketStore, error)

BucketStore builds the per-bucket store this entry's S3 settings describe. The zero store (no endpoint, no region, no keys) means the ambient configuration.

func (ServerEntry) IsPostgres added in v0.33.0

func (e ServerEntry) IsPostgres() bool

IsPostgres reports whether the entry's source is PostgreSQL.

func (ServerEntry) SourceFlavor added in v0.33.0

func (e ServerEntry) SourceFlavor() string

SourceFlavor returns the entry's normalized source family, defaulting a blank (pre-#1019) entry to mysql. Consumed by the monitor supervisor (#1020), the baseline trigger (#1021), and the console UI gating. A stored value that no longer normalizes (only reachable via a hand-edit — create/update validate on write) degrades to mysql, which fails loudly at doctor time rather than silently corrupting anything.

type TelemetryController added in v0.42.0

type TelemetryController interface {
	Enabled() bool
	Decision() telemetry.Decision
	SetRuntimeConsent(enabled bool)
	// RecordDaemonCommand starts a span for one console action. The daemon
	// variant is mandatory here: the console shares the daemon's months-lived
	// run_id, so a plain command span would turn per-action events into a
	// per-install activity timeline. Returns nil when telemetry is off.
	RecordDaemonCommand(command string) *telemetry.Span
}

TelemetryController is the live usage-telemetry surface a long-running console wires in, so the UI opt-out toggle takes effect on the running process immediately instead of only on the next start. `watch` wires *telemetry.Client directly; the read-only `serve` wires a consent-only adapter whose RecordDaemonCommand returns nil, because it beacons (#1362) but must not record console actions. nil means no live process to control — the toggle still persists the machine-wide choice to the consent file.

type VerifyCellDiff added in v0.26.0

type VerifyCellDiff struct {
	Column   string `json:"column"`
	Recovery string `json:"recovery"`
	Baseline string `json:"baseline"`
}

VerifyCellDiff is the wire view of verify.CellDiff.

type VerifyController added in v0.26.0

type VerifyController interface {
	// Trigger starts a verify run in the background and returns immediately.
	// Returns ErrVerifyRunning if one is already running for req.ServerID.
	Trigger(req VerifyRequest) error
	// Status reports the latest known run for a server (idle if never run in
	// this process): overall state plus per-table results accumulated so far.
	Status(serverID string) VerifyStatus
	// Explain drills into one table the last completed baseline-anchored run
	// reported as a mismatch, re-running the row-level diff on demand — never
	// precomputed for every mismatch (internal/verify.ExplainBaselinePairMismatch
	// re-reconstructs the table). Returns ErrExplainUnavailable when no such
	// run/table exists (including: the last run was live-source, which has no
	// explain support in the engine).
	//
	// It MUST NOT block on the reconstruction (#1375): implementations start
	// the work in the background and return ErrExplainRunning immediately,
	// answering with the result on a later call. The reconstruction takes
	// minutes on a large table, and a synchronous answer cannot outlive a
	// fronting proxy's read timeout — so a blocking implementation makes the
	// feature unusable on exactly the tables an operator most needs it for.
	Explain(serverID, schema, table string) (*VerifyExplanation, error)
}

VerifyController runs bintrail verify's engine in-process for a monitored server (#677) — the same internal/verify functions internal/cli/verify.go calls, looped over a server's tables in a background goroutine so the console can poll per-table results as they land. It is wired in ONLY by `bintrail-console watch` when the operator opts in (BINTRAIL_CONSOLE_VERIFY_TRIGGER=1) or schedules verification (--verify-interval, #1191 — scheduling verify implies wanting verify); nil on the standalone read-only console, where the endpoints refuse with 403 — mirroring how BaselineController gates in-process baseline creation.

type VerifyExplanation added in v0.26.0

type VerifyExplanation struct {
	Schema   string          `json:"schema"`
	Table    string          `json:"table"`
	Anchor   string          `json:"anchor"`
	Total    int             `json:"total"`
	Diffs    []VerifyRowDiff `json:"diffs"`
	Rendered string          `json:"rendered"`
}

VerifyExplanation is the wire view of verify.MismatchExplanation: the structured per-row diffs (Diffs/Total, capped and overflow-summarized exactly like the CLI) plus Rendered — the same text internal/verify's MismatchExplanation.Write produces (including the deferred-type caveat and overflow breakdown, which live in unexported fields and so are only available through the rendered text, not the structured Diffs).

type VerifyHistory added in v0.49.0

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

VerifyHistory is the persisted verify-run history (#1191): one JSON file, capped per server, written atomically (temp file + fsync + rename, 0600) like the server registry. It is console-local state on disk — alongside the registry, the auth file and the managed MCP token — not a new class of console write.

It deliberately lives in a console-local file rather than a table in the index database: scheduled verify covers registry servers, and registry DSNs never receive EnsureSchema/DDL (an existing invariant this must not bend).

func OpenVerifyHistory added in v0.49.0

func OpenVerifyHistory(path string) (*VerifyHistory, error)

OpenVerifyHistory loads the history at path. A missing file is an empty history. A corrupt or newer-versioned file is reported as an error — the caller decides whether to run without history rather than silently truncating a file a newer binary may still want.

func (*VerifyHistory) Append added in v0.49.0

func (h *VerifyHistory) Append(rec VerifyRunRecord) error

Append records one run and saves the file. The per-server cap is enforced here (oldest dropped). Called from the supervisor's finish path and the scheduler's skip path; a save failure is returned for the caller to log — history is an observability aid and must never fail a verify run.

func (*VerifyHistory) Found added in v0.49.0

func (h *VerifyHistory) Found() bool

Found reports whether a history file existed when this history was opened. A consumer that reports on verification activity MUST keep this distinct from an opened-but-empty history: the file is written only by `bintrail-console watch`, so a CLI-only deployment has no history at all, and rendering that absence like "no failed runs" would state a clean verification record for a period nothing ever verified.

func (*VerifyHistory) List added in v0.49.0

func (h *VerifyHistory) List(serverID string) []VerifyRunRecord

List returns the recorded runs for a server, newest first. The returned slice is a copy — callers can hold it across later Appends.

func (*VerifyHistory) ServerIDs added in v0.49.0

func (h *VerifyHistory) ServerIDs() []string

ServerIDs returns the server ids present in the history, sorted, so a consumer can enumerate what was recorded without access to the server registry (a run's server may since have been removed from it).

type VerifyMode added in v0.26.0

type VerifyMode string

VerifyMode selects which internal/verify engine path a run uses.

const (
	// VerifyModeBaselineAnchored compares the two most recent baselines,
	// drift-free — no live source read. The default.
	VerifyModeBaselineAnchored VerifyMode = "baseline-anchored"
	// VerifyModeLiveSource reconstructs each table to a consistent snapshot of
	// the live source and compares. Needs the server's source DSN and reads
	// the whole table off production — the console warns to run it off-peak,
	// matching docs/verify.md.
	VerifyModeLiveSource VerifyMode = "live-source"
	// VerifyModeRecoverInputs walks each primary key's event chain and asserts
	// the before/after images recover consumes are internally consistent — the
	// console face of `bintrail verify --check recover` (#1191). Index-only:
	// needs no baseline and no source DSN, which is why the scheduled runner
	// falls back to it for servers with no baseline configured.
	VerifyModeRecoverInputs VerifyMode = "recover-inputs"
)

type VerifyRequest added in v0.26.0

type VerifyRequest struct {
	ServerID    string
	ServerName  string
	Mode        VerifyMode
	Tables      []string // optional "schema.table" filter; empty = all
	IndexDSN    string
	SourceDSN   string // only required/used for VerifyModeLiveSource
	BaselineDir string
	BaselineS3  string
	NoArchive   bool
}

VerifyRequest is the in-process job description the endpoint hands the controller. The index/source DSNs (secrets) stay inside the process — never written to disk or serialized to any HTTP response.

type VerifyRowDiff added in v0.26.0

type VerifyRowDiff struct {
	PK    string           `json:"pk"`
	Kind  string           `json:"kind"` // changed | missing | extra
	Cells []VerifyCellDiff `json:"cells,omitempty"`
}

VerifyRowDiff is the wire view of verify.RowDiff.

type VerifyRunRecord added in v0.49.0

type VerifyRunRecord struct {
	ServerID   string `json:"server_id"`
	ServerName string `json:"server_name,omitempty"`
	// Trigger records who started the run: "manual" (the POST endpoint) or
	// "scheduled" (the watch daemon's --verify-interval loop, #1191).
	Trigger string `json:"trigger"`
	// SkipReason is set only on records with State "skipped" — a scheduled
	// cycle that could not run this server (e.g. a manual run was already in
	// flight). Recorded rather than dropped so a schedule that never actually
	// verifies is visible in the history, not silent.
	SkipReason string `json:"skip_reason,omitempty"`
	VerifyStatus
}

VerifyRunRecord is one completed (or skipped) verify run as stored in the history file and served by GET /api/servers/{id}/verify/history. It embeds the same VerifyStatus shape the live status endpoint serves, so a consumer renders a historical run and the current one with the same code.

type VerifyStatus added in v0.26.0

type VerifyStatus struct {
	State      string     `json:"state"` // idle | running | succeeded | failed (history records may also carry "skipped" — see VerifyRunRecord)
	Mode       VerifyMode `json:"mode,omitempty"`
	Since      string     `json:"since,omitempty"`
	FinishedAt string     `json:"finished_at,omitempty"`
	LastError  string     `json:"last_error,omitempty"`
	// Note carries a benign, non-error informational message — e.g. "only one
	// baseline exists yet, nothing to compare" (a legitimate first run, not a
	// failure; mirrors the CLI's zero-exit early return in that case).
	Note    string              `json:"note,omitempty"`
	Results []VerifyTableResult `json:"results,omitempty"`
	Summary VerifySummary       `json:"summary"`
}

VerifyStatus is the pollable state of a server's most recent verify run. Unlike BaselineStatus (one terminal struct overwritten at job end), Results grows as each table completes so the console can show progress mid-run — internal/verify has no progress callback of its own; the controller loops over tables itself and appends after each one returns.

type VerifySummary added in v0.26.0

type VerifySummary struct {
	Match        int `json:"match"`
	Mismatch     int `json:"mismatch"`
	Inconclusive int `json:"inconclusive"`
	// InconclusiveNothingToCheck mirrors verify.Summary's benign slice of
	// Inconclusive (#1416): quiet or append-only tables where zero assertions
	// is the expected outcome. Always <= Inconclusive.
	InconclusiveNothingToCheck int `json:"inconclusive_nothing_to_check"`
	Error                      int `json:"error"`
	// Total is the number of results tallied — the same `total` the CLI's
	// `verify --format json` summary carries.
	Total int `json:"total"`
}

VerifySummary tallies VerifyStatus.Results by status, for a run's headline. Its fields mirror verify.Summary EXACTLY — same names, types and order — because consoleapp's supervisor tallies through verify.Summary.Count (the one bucket classification, #1127) and publishes here via a struct conversion. The compiler rejects that conversion when field names, order, or types drift; struct TAGS are ignored by conversion identity, so a JSON tag rename on either side would NOT be caught — keep the tags in sync by hand. It stays a distinct type only because this package must not import internal/verify (the read layer must not link the capture library — see internal/event's dep guard).

type VerifyTableResult added in v0.26.0

type VerifyTableResult struct {
	Schema string `json:"schema"`
	Table  string `json:"table"`
	// Status is normalized through verify.NormalizeStatus before it reaches
	// the wire — the same status→bucket decision the CLI's JSON report uses,
	// so an unrecognized engine status surfaces as "error", never as a benign
	// value (#1127).
	Status string `json:"status"` // match | mismatch | inconclusive | error
	// Reason is the detail behind the verdict — the same datum, under the same
	// name, as the CLI's `verify --format json` per-table `reason`.
	Reason string `json:"reason,omitempty"`
	// Detail is the legacy alias for Reason, kept for consumers of the
	// original #677 wire shape. Always carries the same value as Reason.
	Detail string `json:"detail,omitempty"`
	// InconclusiveKind subdivides an inconclusive verdict (#1416):
	// no-activity | nothing-to-assert | unproven. Empty otherwise.
	InconclusiveKind string `json:"inconclusive_kind,omitempty"`
	SourceRows       int64  `json:"source_rows,omitempty"`
	ReconstructRows  int64  `json:"reconstruct_rows,omitempty"`
	// EventsChecked/ChainsChecked: the recover-inputs walk's per-table
	// counters, same tag names as the CLI's verify --format json (#1425
	// review: the console rendered a counts column these fields never
	// reached — toWireResult dropped them on the floor). Zero-omitted; the
	// content modes carry neither.
	EventsChecked int    `json:"events_checked,omitempty"`
	ChainsChecked int    `json:"chains_checked,omitempty"`
	Anchor        string `json:"anchor,omitempty"`
	// Explainable is true only for a baseline-anchored mismatch whose pair is
	// still cached from the run that produced this result — the precondition
	// for calling Explain on it.
	Explainable bool `json:"explainable"`
}

VerifyTableResult is the wire view of one table's verify.TableResult.

Jump to

Keyboard shortcuts

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