Files
nmon/internal/store/components.go
jleung e25143881c Device metrics API, printer fleet, profile presentation schema (v1.0.1 backend)
Profiles gain category/label/of/display/scale_by/sentinels/enum_state and component_attrs;
every pollable non-interface metric is annotated and an orphan test enforces it. New routes
GET /devices/{slug}/metrics, /metrics/graph and /printers; printer supply type/colour read at
discovery into device_components.attrs; printer_supply_empty alert; supply_replaced audit event;
docs/coverage.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-23 05:33:07 +00:00

98 lines
3.3 KiB
Go

package store
import (
"context"
"time"
"github.com/uptrace/bun"
)
// Component is one row of a profile table on a device: Idx is the OID index
// suffix (e.g. "1" or "1.3"), Label the value of the profile's index_label column.
type Component struct {
bun.BaseModel `bun:"table:device_components,alias:dc"`
DeviceID int64 `bun:"device_id,pk"`
TableName string `bun:"table_name,pk"`
Idx string `bun:"idx,pk"`
Label string `bun:"label,notnull"`
// Attrs is a JSON object of discovery-time attributes ("{}" when none), from the profile's component_attrs.
Attrs string `bun:"attrs,notnull,default:'{}'"`
FirstSeen time.Time `bun:"first_seen,notnull"`
RetiredAt *time.Time `bun:"retired_at"`
}
// Components stores discovered table rows.
type Components interface {
// ReplaceComponents makes rows the current set for (deviceID, table): new rows
// are inserted, existing rows updated (label, un-retired), missing rows retired.
ReplaceComponents(ctx context.Context, deviceID int64, table string, rows []Component) error
// ListComponents returns a device's active rows, all tables, ordered by table then index.
ListComponents(ctx context.Context, deviceID int64) ([]Component, error)
}
func (s *bunStore) ReplaceComponents(ctx context.Context, deviceID int64, table string, rows []Component) error {
return s.WithTx(ctx, func(tx Store) error {
t := tx.(*bunStore)
var cur []Component
if err := t.db.NewSelect().Model(&cur).Where("device_id = ? AND table_name = ?", deviceID, table).Scan(ctx); err != nil {
return mapErr(err)
}
now := s.now()
if err := t.upsertComponents(ctx, deviceID, table, rows, cur, now); err != nil {
return err
}
return t.retireMissingComponents(ctx, rows, cur, now)
})
}
// upsertComponents inserts rows that are new and refreshes (label, un-retire) those already in cur.
func (s *bunStore) upsertComponents(ctx context.Context, deviceID int64, table string, rows, cur []Component, now time.Time) error {
have := make(map[string]Component, len(cur))
for _, c := range cur {
have[c.Idx] = c
}
for _, r := range rows {
r.DeviceID, r.TableName = deviceID, table
if r.Attrs == "" {
r.Attrs = "{}"
}
if old, ok := have[r.Idx]; ok {
r.FirstSeen = old.FirstSeen
if _, err := s.db.NewUpdate().Model(&r).Column("label", "attrs", "retired_at").WherePK().Exec(ctx); err != nil {
return mapErr(err)
}
continue
}
r.FirstSeen, r.RetiredAt = now, nil
if _, err := s.db.NewInsert().Model(&r).Exec(ctx); err != nil {
return mapErr(err)
}
}
return nil
}
// retireMissingComponents retires the rows of cur that rows no longer lists.
func (s *bunStore) retireMissingComponents(ctx context.Context, rows, cur []Component, now time.Time) error {
seen := make(map[string]bool, len(rows))
for _, r := range rows {
seen[r.Idx] = true
}
for _, c := range cur {
if seen[c.Idx] || c.RetiredAt != nil {
continue
}
if _, err := s.db.NewUpdate().Model(&c).Set("retired_at = ?", now).WherePK().Exec(ctx); err != nil {
return mapErr(err)
}
}
return nil
}
func (s *bunStore) ListComponents(ctx context.Context, deviceID int64) ([]Component, error) {
var rows []Component
err := s.db.NewSelect().Model(&rows).Where("device_id = ? AND retired_at IS NULL", deviceID).
OrderExpr("table_name, idx").Scan(ctx)
return rows, mapErr(err)
}