insights

package
v0.65.2 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package insights persists saved reports and the boards they live on — the "pin this report" feature that turns ad-hoc Explore into a dashboard you open every morning. Config-shaped (small, mutable), so it's a single JSON file rewritten atomically on change, separate from the append-only event log.

Index

Constants

View Source
const DefaultBoardID = "default"

DefaultBoardID is the board every insight falls back to: the home for a Save that names no board, and the board an existing installation's saved reports join on upgrade. It is a fixed string rather than a generated id precisely so the upgrade is idempotent — re-opening an already-migrated file finds this same board instead of minting a second "default" every launch.

Variables

View Source
var (
	ErrNoSuchBoard    = errors.New("board not found")
	ErrNoSuchInsight  = errors.New("insight not found")
	ErrBoardNotEmpty  = errors.New("board still has insights")
	ErrDefaultBoard   = errors.New("the default board cannot be deleted")
	ErrBoardNameTaken = errors.New("a board with that name already exists")
)

Sentinel errors so callers (the HTTP layer especially) can map a refusal to the right status code instead of string-matching a message.

Functions

This section is empty.

Types

type Board added in v0.29.0

type Board struct {
	ID      string    `json:"id"`
	Name    string    `json:"name"`
	Created time.Time `json:"created"`
	// Filters are applied to every insight on the board. An insight that sets the
	// same key wins — a board default must never silently overwrite a filter the
	// user typed into the report itself. MergeFilters is that rule; call it rather
	// than re-implementing the precedence per surface.
	Filters map[string]string `json:"filters,omitempty"`
	Range   TimeRange         `json:"range"`
}

Board is one dashboard: a named collection of saved insights plus the defaults every insight on it inherits when it is rendered — the board's own filters and its time range. That inheritance is the whole point of a board: "Growth, last 7 days, plan=pro" is one setting on the board, not a setting repeated on each of its nine cards and forgotten on the tenth.

func (Board) MergeFilters added in v0.29.0

func (b Board) MergeFilters(own map[string]string) map[string]string

MergeFilters folds the board's default filters underneath a card's own and returns the set the query should actually run with. A key the card sets WINS: a board default is a convenience, and one that silently overwrote a filter the user typed into the report itself would answer a different question than the card's title claims.

It takes the card's filters as a plain map so this package never has to know the query layer's chip grammar — the caller parses, this decides precedence, and the rule therefore exists in one place rather than once per calling surface.

type BoardCard added in v0.29.0

type BoardCard struct {
	Insight
	// OwnWindow is true when the saved params already name a window (days/hours/
	// from/to). The board's range must NOT be applied to such a card: the user
	// deliberately saved "signups, last 24h", and silently stretching that to the
	// board's 30 days would print a different number under the same title.
	OwnWindow bool `json:"own_window"`
}

BoardCard is one insight as a board will draw it, plus the one thing the render layer would otherwise have to work out for itself: whether this card already pins a window of its own.

type BoardView added in v0.29.0

type BoardView struct {
	Board Board         `json:"board"`
	Range ResolvedRange `json:"range"`
	Cards []BoardCard   `json:"cards"`
}

BoardView is one dashboard render in a single read: the board, the window its cards cover, and the cards themselves. Range is stated rather than implied, because a dashboard that shows numbers without saying which window produced them is unciteable — the same rule every report payload here follows.

type Insight

type Insight struct {
	ID      string            `json:"id"`
	Name    string            `json:"name"`
	Type    string            `json:"type"`
	Params  map[string]string `json:"params"`
	BoardID string            `json:"board_id"`
	Created time.Time         `json:"created"`
}

Insight is one saved report: a type (funnel|trend|breakdown|retention) plus the params Explore needs to re-run it. BoardID says which board it appears on; it is never empty in the store (Save fills it, load adopts orphans), so a board render never has to guess where a row belongs.

type ResolvedRange added in v0.29.0

type ResolvedRange struct {
	From   time.Time `json:"from"`
	To     time.Time `json:"to"`
	Days   int       `json:"days,omitempty"`
	Source string    `json:"source"` // "relative" | "absolute" | "default"
}

ResolvedRange is a TimeRange turned into the actual half-open window [From, To) a query runs over, carrying enough of its own provenance that a response can state it: which instants, how wide, and whether they came from the board's relative setting, its fixed dates, or the store default. A dashboard that shows numbers without saying what window produced them is unciteable.

type Store

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

Store holds saved insights and boards in memory and (when path != "") persists them.

func Open

func Open(path string) (*Store, error)

Open loads saved insights from path (empty/missing = start fresh). An empty path means in-memory only (used by the throwaway demo).

func (*Store) Boards added in v0.29.0

func (s *Store) Boards() []Board

Boards returns every board, default first. Order is stable across loads because adoptOrphans always prepends the default board and creation appends.

func (*Store) CountByBoard added in v0.29.0

func (s *Store) CountByBoard() map[string]int

CountByBoard returns how many insights sit on each board, including zero for empty boards. The board list is the first thing the dashboard draws, and without this it would fan out one ListBoard per board to print "4 insights".

func (*Store) CreateBoard added in v0.29.0

func (s *Store) CreateBoard(b Board) (Board, error)

CreateBoard adds a board and persists. The caller's ID and Created are ignored — like Save, the store owns identity, so a client cannot mint a second board with an id that already means something else.

func (*Store) Delete

func (s *Store) Delete(id string) (found bool, err error)

Delete removes an insight by id (no error if it's already gone). found is true only when a row actually went away, so callers never claim a removal that did not occur.

func (*Store) DeleteBoard added in v0.29.0

func (s *Store) DeleteBoard(id string) error

DeleteBoard removes an empty board.

The orphan question has exactly two honest answers — refuse, or reparent — and this store REFUSES: deleting a board that still holds insights returns ErrBoardNotEmpty and changes nothing. Reparenting silently would make one click on "delete board" scatter a user's saved work onto a board they weren't looking at, and they'd have no way to tell it apart from the work being gone. The deliberate path is MoveAllInsights (or Delete per insight) first, which makes the caller say where the work should go.

The default board is never deletable: every insight must name a board that exists, and the default is the board that is always there to name.

func (*Store) GetBoard added in v0.29.0

func (s *Store) GetBoard(id string) (Board, error)

GetBoard returns one board by id.

func (*Store) List

func (s *Store) List() []Insight

List returns every insight across every board, oldest first. Kept board-agnostic because the MCP/API "list saved reports" surfaces predate boards and must not start hiding rows.

func (*Store) ListBoard added in v0.29.0

func (s *Store) ListBoard(boardID string) ([]Insight, error)

ListBoard returns the insights pinned to one board, in save order. An unknown board is an error rather than an empty list: silently rendering "no insights yet" for a board that does not exist is how a typo reads as data loss.

func (*Store) MoveAllInsights added in v0.29.0

func (s *Store) MoveAllInsights(fromBoard, toBoard string) (moved int, err error)

MoveAllInsights repins every insight on one board to another, returning how many moved. This is the deliberate way to empty a board before deleting it — see DeleteBoard, which refuses while insights remain.

func (*Store) MoveInsight added in v0.29.0

func (s *Store) MoveInsight(insightID, boardID string) (Insight, error)

MoveInsight repins one insight to another board. Moving to the board it already sits on is a no-op that does not touch the file — rewriting on a no-op drag is pointless disk churn and a pointless chance to fail.

func (*Store) Render added in v0.29.0

func (s *Store) Render(boardID string, now time.Time) (BoardView, error)

Render assembles everything one board render needs. now is a parameter and never time.Now(): the resolved window is echoed to the client and pinned by tests, and a function that reads the clock itself can deliver neither a reproducible test nor a window the caller can verify.

func (*Store) Save

func (s *Store) Save(in Insight) (Insight, error)

Save validates and stores an insight (assigning an id), then persists. An empty BoardID means the default board; a named board must already exist. Returns the stored copy.

func (*Store) UpdateBoard added in v0.29.0

func (s *Store) UpdateBoard(id string, in Board) (Board, error)

UpdateBoard renames a board and/or replaces its defaults. Name, Filters and Range are taken from in; ID and Created are not editable. An empty name is rejected rather than treated as "leave it alone" — a board whose name vanished is a board the user cannot find in the switcher.

The default board can be renamed (its name is just a label) but not deleted; see DeleteBoard.

type TimeRange added in v0.29.0

type TimeRange struct {
	Days int       `json:"days,omitempty"`
	From time.Time `json:"from,omitempty"`
	To   time.Time `json:"to,omitempty"`
}

TimeRange is a board's default window, stored the way the user chose it: either relative ("last 30 days") or a fixed pair of instants. It is deliberately NOT resolved at rest — a board saved as "last 30 days" has to mean the 30 days before the morning you open it, not the 30 days before the afternoon you created it, which is what storing absolute instants for a relative choice would freeze it into.

func (TimeRange) Resolve added in v0.29.0

func (r TimeRange) Resolve(now time.Time) (ResolvedRange, error)

Resolve turns the stored range into the concrete window to query. now is a parameter and never time.Now(): the resolved window is echoed back in the API response and pinned by tests, and a function that reads the clock itself can deliver neither a reproducible test nor a window the caller can verify.

The window is half-open — [From, To) — matching every other window in this codebase, so an event landing exactly on the boundary is counted once rather than in both of two adjacent ranges.

Jump to

Keyboard shortcuts

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