···379379 return nil
380380}
381381382382-// BackfillKnownUsers backfills records for all known DIDs
383383-// This is useful on startup to ensure we have all existing records
384384-func (c *Consumer) BackfillKnownUsers(ctx context.Context) error {
385385- dids, err := c.index.GetKnownDIDs()
386386- if err != nil {
387387- return fmt.Errorf("failed to get known DIDs: %w", err)
388388- }
389389-390390- log.Info().Int("count", len(dids)).Msg("firehose: backfilling known users")
391391-392392- for _, did := range dids {
393393- select {
394394- case <-ctx.Done():
395395- return ctx.Err()
396396- default:
397397- }
398398-399399- if err := c.index.BackfillUser(ctx, did); err != nil {
400400- log.Warn().Err(err).Str("did", did).Msg("firehose: failed to backfill user")
401401- }
402402-403403- // Small delay to avoid hammering PDS servers
404404- time.Sleep(100 * time.Millisecond)
405405- }
406406-407407- return nil
408408-}
409409-410382// BackfillDID backfills records for a specific DID
411383func (c *Consumer) BackfillDID(ctx context.Context, did string) error {
412384 return c.index.BackfillUser(ctx, did)
···3434 return fmt.Sprintf("%.1f°%c", temp, unit)
3535}
36363737-// FormatTempValue formats a temperature for use in input fields (numeric value only).
3838-func FormatTempValue(temp float64) string {
3939- return fmt.Sprintf("%.1f", temp)
4040-}
4141-4237// FormatTime formats seconds into a human-readable time string (e.g., "3m 30s").
4338// Returns "N/A" if seconds is 0.
4439func FormatTime(seconds int) string {
···6560 return fmt.Sprintf("%d/10", rating)
6661}
67626868-// FormatID converts an int to string.
6969-func FormatID(id int) string {
7070- return fmt.Sprintf("%d", id)
7171-}
7272-7373-// FormatInt converts an int to string.
7474-func FormatInt(val int) string {
7575- return fmt.Sprintf("%d", val)
7676-}
7777-7878-// FormatRoasterID formats a nullable roaster ID.
7979-// Returns "null" if id is nil, otherwise the ID as a string.
8080-func FormatRoasterID(id *int) string {
8181- if id == nil {
8282- return "null"
8383- }
8484- return fmt.Sprintf("%d", *id)
8585-}
8686-8763// PoursToJSON serializes a slice of pours to JSON for use in JavaScript.
8864func PoursToJSON(pours []*models.Pour) string {
8965 if len(pours) == 0 {
···11187 return string(jsonBytes)
11288}
11389114114-// Ptr returns a pointer to the given value.
115115-func Ptr[T any](v T) *T {
116116- return &v
117117-}
118118-119119-// PtrEquals checks if a pointer equals a value.
120120-// Returns false if the pointer is nil.
121121-func PtrEquals[T comparable](p *T, val T) bool {
122122- if p == nil {
123123- return false
124124- }
125125- return *p == val
126126-}
127127-128128-// PtrValue returns the dereferenced value of a pointer, or zero value if nil.
129129-func PtrValue[T any](p *T) T {
130130- if p == nil {
131131- var zero T
132132- return zero
133133- }
134134- return *p
135135-}
136136-137137-// Iterate returns a slice of ints from 0 to n-1, useful for range loops in templates.
138138-func Iterate(n int) []int {
139139- result := make([]int, n)
140140- for i := range result {
141141- result[i] = i
142142- }
143143- return result
144144-}
145145-146146-// IterateRemaining returns a slice of ints for the remaining count, useful for star ratings.
147147-// For example, IterateRemaining(3, 5) returns [0, 1] for the 2 remaining empty stars.
148148-func IterateRemaining(current, total int) []int {
149149- remaining := total - current
150150- if remaining <= 0 {
151151- return nil
152152- }
153153- result := make([]int, remaining)
154154- for i := range result {
155155- result[i] = i
156156- }
157157- return result
158158-}
159159-16090// HasTemp returns true if temperature is greater than zero
16191func HasTemp(temp float64) bool {
16292 return temp > 0
···252182 s = strings.ReplaceAll(s, "\r", "\\r")
253183 s = strings.ReplaceAll(s, "\t", "\\t")
254184 return s
255255-}
256256-257257-// Dict creates a map from alternating key-value arguments.
258258-// Useful for passing multiple parameters to sub-templates in Go templates.
259259-// Example: {{template "foo" dict "Key1" .Value1 "Key2" .Value2}}
260260-func Dict(values ...interface{}) (map[string]interface{}, error) {
261261- if len(values)%2 != 0 {
262262- return nil, fmt.Errorf("dict requires an even number of arguments")
263263- }
264264- dict := make(map[string]interface{}, len(values)/2)
265265- for i := 0; i < len(values); i += 2 {
266266- key, ok := values[i].(string)
267267- if !ok {
268268- return nil, fmt.Errorf("dict keys must be strings")
269269- }
270270- dict[key] = values[i+1]
271271- }
272272- return dict, nil
273185}
274186275187// FormatTimeAgo returns a human-readable relative time string