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
98 lines
3.3 KiB
Go
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)
|
|
}
|