Items-list sparklines, retro CSS, pinned tooling, deploy docs

- Bulk-load recent price points per item and render a sparkline in
  the items list (new LoadRecentPriceHistory query avoids N+1).
- Add retro.css visual layer and refreshed login/items/layout styling.
- Swap the logo from webp to avif.
- Pin htmx/Chart.js/Tailwind/templ versions in the Makefile with
  vendor / tools / update-deps targets; README documents the
  dependency-bump flow and the hardened systemd deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-15 19:10:56 -07:00
parent 0ec97afafb
commit ea3577a45e
17 changed files with 1174 additions and 343 deletions

View File

@@ -798,6 +798,49 @@ func (s *Store) InsertPricePoint(ctx context.Context, p *models.PricePoint) erro
return err
}
// LoadRecentPriceHistory bulk-loads the last `perItem` price points (oldest →
// newest) for each item id. Returns a map keyed by item id; items with no
// history are absent from the map. Used by the items list page to render a
// sparkline per row without N+1 queries.
func (s *Store) LoadRecentPriceHistory(ctx context.Context, ids []int64, perItem int) (map[int64][]models.PricePoint, error) {
out := make(map[int64][]models.PricePoint, len(ids))
if len(ids) == 0 || perItem <= 0 {
return out, nil
}
placeholders := make([]string, len(ids))
args := make([]any, 0, len(ids)+1)
for i, id := range ids {
placeholders[i] = "?"
args = append(args, id)
}
args = append(args, perItem)
q := `
WITH ranked AS (
SELECT item_id, price, store, polled_at,
ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY polled_at DESC) AS rn
FROM price_history
WHERE item_id IN (` + strings.Join(placeholders, ",") + `)
)
SELECT item_id, price, store, polled_at FROM ranked WHERE rn <= ?
ORDER BY item_id, polled_at ASC
`
rows, err := s.DB.QueryContext(ctx, q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var p models.PricePoint
var store sql.NullString
if err := rows.Scan(&p.ItemID, &p.Price, &store, &p.PolledAt); err != nil {
return nil, err
}
p.Store = s.dec(store.String)
out[p.ItemID] = append(out[p.ItemID], p)
}
return out, rows.Err()
}
func (s *Store) ListPriceHistory(ctx context.Context, itemID int64) ([]models.PricePoint, error) {
rows, err := s.DB.QueryContext(ctx,
`SELECT id, item_id, price, store, polled_at FROM price_history WHERE item_id = ? ORDER BY polled_at ASC`,