Documentation
¶
Overview ¶
Package dashboards owns the interpretation of dashboard rows: the config document (HTML + named datasources) and its validation rules, name→row resolution, and datasource execution into the {columns, rows, truncated, warning?} document the render iframe draws from (GDK-781).
Two surfaces speak dashboards — `gadak dashboards` (cmd/gadak) and the /api/v1/dashboards/ handlers (internal/server) — and both import this package for the same reason internal/views exists (GDK-612): the rule and the row shape must have one owner, not a CLI copy and an API copy that drift.
SQL datasources run only on a read-only connection handed in by the caller (store.DB.ReadOnly). That is not a convenience choice: arbitrary SQL is allowed precisely because it can never take a write path — the mirror is a cache of the origin, and this package is where that stays true.
Index ¶
- Constants
- Variables
- func FindDashboard(db *store.DB, name string) (store.Dashboard, error)
- func LibRemove(dir, id string) error
- func LibsDir(profileDir string) string
- func LibsExist(dir string, ids []string) (missing []string, err error)
- func ValidLibID(id string) bool
- func ValidName(name string) bool
- type Config
- type Lib
- type Result
- type Source
Constants ¶
const ( MaxRows = 10000 MaxRowBytes = 2 << 20 // 2 MiB of marshaled row payload )
Row ceilings for datasource execution (server halves of the same contract). Generous on purpose — this is a performance guard for a local single-user tool, not a quota: a triage wall wants thousands of rows long before anyone writes a query that returns them.
const ( // 50 MiB (GDK-808, user call 2026-08-24): the cap is a mistake guard on a // user-invoked download, not a security boundary — sized so a whole built // app bundle can ride as one lib. MaxLibBytes = 50 << 20 MaxLibRedirects = 3 MaxLibs = 8 )
Cache limits. MaxLibBytes bounds both a lying Content-Length and an infinite stream (the reader is capped one byte above, so a body at the limit is read to that point and refused, never buffered unbounded). Redirects are bounded because a redirect chain is attacker-controlled routing, not content. The timeout reuses the origin clients' budget (httppolicy.DefaultTimeout): one user-invoked fetch, no retries — a 5xx is an answer, and retrying would re-download bytes the user pays for. The rest of httppolicy (retryable statuses, the 64 MiB MaxBody) belongs to API clients; this is not one.
const LibIDPattern = `[a-f0-9]{16}-[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9_-])?`
LibIDPattern is the lib id rule: 16 hex of the content's sha256, a dash, then the sanitized original basename. The id is also the cache filename, so the pattern doubles as the path-safety contract — no separators, no "..", nothing that can escape libs/. Quoted in violation messages like NamePattern so an author can fix the config without a second round-trip.
const NamePattern = `[a-z0-9][a-z0-9_-]{0,63}`
NamePattern is the datasource-name rule, quoted verbatim in every violation message so an agent can fix the name without a second round-trip.
Variables ¶
var ErrLibCorrupt = errors.New("lib cache corrupt")
ErrLibCorrupt means bytes exist but do not match their pinned hash (or the manifest itself does not parse) — an operator problem, surfaced as a 500 by the serve route, never as a silently-served file.
var ErrLibNotFound = errors.New("lib not found")
ErrLibNotFound is returned by LibLookup/LibRemove for an id the manifest does not carry. The serve route answers 404 on it, like the vendor route.
Functions ¶
func FindDashboard ¶
FindDashboard resolves one name to one row: exact id or exact name (case-insensitive) first, then a single substring hit. Same resolution policy as views.FindView so `gadak dashboards show triage` and a future MCP tool behave alike; only the miss policy is dashboard-specific (the message names the command that lists what exists).
func LibsDir ¶
LibsDir is the cache directory under a profile directory. Single owner of the path shape: the CLI passes config.Dir(), the server config.DirFor of its profile — both get "dashboards/libs" from here, never spelled twice.
func LibsExist ¶
LibsExist reports which of ids the cache does not carry — the save paths' existence check. A manifest that fails to parse names no lib (every id is "missing"), which is fail-closed: nothing saves against an unreadable pin.
func ValidLibID ¶
ValidLibID is the lib id rule used by ParseConfig and by the manifest loader (a hand-edited manifest must not smuggle a path through the id).
Types ¶
type Config ¶
type Config struct {
HTML string `json:"html"`
Datasources map[string]Source `json:"datasources,omitempty"`
Libs []string `json:"libs,omitempty"`
}
Config is the document a dashboard row stores. Datasources may be empty — a static HTML dashboard is valid — but never nil after ParseConfig. Libs (GDK-808) names cache entries from `gadak dashboards lib add`; existence is checked by the save paths (CLI and API), not here — ParseConfig owns the shape, callers own the world.
func ParseConfig ¶
ParseConfig decodes and validates a stored config document. It is strict on purpose: every field it accepts is a field render/data must honor, so an unknown key (an older config shape, a typo'd hand edit) is a named error, not a silently ignored clause.
type Lib ¶
type Lib struct {
ID string `json:"id"`
URL string `json:"url"`
SHA384 string `json:"sha384"`
Size int64 `json:"size"`
FetchedAt string `json:"fetched_at"`
}
Lib is one manifest entry. ID is also the filename inside the cache dir, so entry and bytes cannot drift apart.
func LibAdd ¶
func LibAdd(ctx context.Context, dir, rawURL string, replace bool, now time.Time) (lib Lib, added bool, err error)
LibAdd downloads rawURL once and caches it. added is false when the exact bytes are already cached (same URL and sha384 — the idempotent re-run). Same URL with a different hash is an upstream change: refused unless replace, because a dashboard's config pins the old id and a silent swap would change what its next render executes.
func LibReadVerified ¶
LibReadVerified is the serve path's whole trust decision: read the bytes, re-hash them, and hand them over only when they still match the pin. The manifest itself not parsing is ErrLibCorrupt here too — a cache whose pin cannot be read serves nothing. Size is checked first because it is free and catches truncation without hashing.
type Result ¶
type Result struct {
Columns []string `json:"columns"`
Rows [][]any `json:"rows"`
Truncated bool `json:"truncated"`
Warning string `json:"warning,omitempty"`
}
Result is the datasource execution document. It is also the postMessage payload contract for the web round: {type:'data', name, columns, rows, truncated, warning?} carries these fields verbatim.
func ExecuteJQL ¶
ExecuteJQL runs one datasource JQL query against the mirror: parse, resolve currentUser() against the configured identity and the actor directory, match, apply the ORDER BY, and project to jqlColumns. It is the server jql execution path (internal/server jql.go's parse+identity) with the CLI's match step (cmd/gadak searchJQL) — the two halves this feature finally needs in one place.
func ExecuteSQL ¶
ExecuteSQL runs one datasource SQL statement on ro, which callers must open through store.DB.ReadOnly — the read-only connection is what makes arbitrary SQL safe to allow. Zero rows plus a display-name comparison yields the sqlhint warning verbatim (the locale trap, surfaced where the agent will see it instead of as a mystery empty card).