Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: new button in nav

+131 -3
+18 -2
cmd/server/main.go
··· 291 291 } 292 292 } 293 293 294 + // Pre-load already-backfilled DIDs to avoid N+1 SELECT per DID 295 + alreadyBackfilled, err := firehoseConsumer.BackfilledDIDs(ctx) 296 + if err != nil { 297 + log.Warn().Err(err).Msg("Failed to load backfilled DIDs, will check individually") 298 + alreadyBackfilled = make(map[string]struct{}) 299 + } 300 + 301 + // Remove already-backfilled DIDs 302 + for did := range alreadyBackfilled { 303 + delete(didsToBackfill, did) 304 + } 305 + 294 306 // Create a root span so all backfill PDS calls are grouped under one trace 295 307 backfillCtx, backfillSpan := tracing.HandlerSpan(ctx, "backfill.startup") 296 308 297 - // Backfill all collected DIDs 309 + // Backfill remaining DIDs 298 310 successCount := 0 299 311 for did := range didsToBackfill { 300 312 if err := firehoseConsumer.BackfillDID(backfillCtx, did); err != nil { ··· 304 316 } 305 317 } 306 318 backfillSpan.End() 307 - log.Info().Int("total", len(didsToBackfill)).Int("success", successCount).Msg("Backfill complete") 319 + log.Info(). 320 + Int("skipped", len(alreadyBackfilled)). 321 + Int("backfilled", successCount). 322 + Int("failed", len(didsToBackfill)-successCount). 323 + Msg("Backfill complete") 308 324 }() 309 325 310 326 // Register users in the feed when they authenticate
+5
internal/firehose/consumer.go
··· 451 451 func (c *Consumer) BackfillDID(ctx context.Context, did string) error { 452 452 return c.index.BackfillUser(ctx, did) 453 453 } 454 + 455 + // BackfilledDIDs returns the set of all DIDs that have been backfilled. 456 + func (c *Consumer) BackfilledDIDs(ctx context.Context) (map[string]struct{}, error) { 457 + return c.index.BackfilledDIDs(ctx) 458 + }
+19
internal/firehose/index.go
··· 1446 1446 return err == nil 1447 1447 } 1448 1448 1449 + // BackfilledDIDs returns the set of all DIDs that have been backfilled. 1450 + func (idx *FeedIndex) BackfilledDIDs(ctx context.Context) (map[string]struct{}, error) { 1451 + rows, err := idx.db.QueryContext(ctx, `SELECT did FROM backfilled`) 1452 + if err != nil { 1453 + return nil, err 1454 + } 1455 + defer rows.Close() 1456 + 1457 + result := make(map[string]struct{}) 1458 + for rows.Next() { 1459 + var did string 1460 + if err := rows.Scan(&did); err != nil { 1461 + return nil, err 1462 + } 1463 + result[did] = struct{}{} 1464 + } 1465 + return result, rows.Err() 1466 + } 1467 + 1449 1468 // MarkBackfilled marks a DID as backfilled with current timestamp 1450 1469 func (idx *FeedIndex) MarkBackfilled(ctx context.Context, did string) error { 1451 1470 _, err := idx.db.ExecContext(ctx, `INSERT OR IGNORE INTO backfilled (did, backfilled_at) VALUES (?, ?)`,
+89 -1
internal/web/components/header.templ
··· 1 1 package components 2 2 3 3 import ( 4 - "fmt" 5 4 "arabica/internal/web/bff" 5 + "fmt" 6 6 ) 7 7 8 8 // HeaderProps contains all properties for the header component ··· 45 45 </span> 46 46 } 47 47 </a> 48 + <!-- Create new dropdown --> 49 + <div x-data="{ open: false }" class="relative"> 50 + <button @click="open = !open" @click.outside="open = false" class="hover:opacity-80 transition p-1 focus:outline-none" title="Create new"> 51 + <svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"> 52 + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"></path> 53 + </svg> 54 + </button> 55 + <div x-show="open" x-cloak x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="dropdown-menu w-52"> 56 + <div class="dropdown-header"> 57 + <p class="text-xs font-semibold uppercase tracking-wider text-brown-500">Log</p> 58 + </div> 59 + <a href="/brews/new" class="dropdown-item flex items-center gap-2" @click="open = false"> 60 + @IconCoffee() 61 + New Brew 62 + </a> 63 + // <button 64 + // class="dropdown-item flex items-center gap-2 w-full text-left" 65 + // hx-get="/api/modals/drink/new" 66 + // hx-target="#modal-container" 67 + // hx-swap="innerHTML" 68 + // @click="open = false" 69 + // > 70 + // @IconDroplet() Log Drink 71 + // </button> 72 + <div class="dropdown-divider"> 73 + <p class="px-4 pt-1 text-xs font-semibold uppercase tracking-wider text-brown-500">Add</p> 74 + </div> 75 + <button 76 + class="dropdown-item flex items-center gap-2 w-full text-left" 77 + hx-get="/api/modals/bean/new" 78 + hx-target="#modal-container" 79 + hx-swap="innerHTML" 80 + @click="open = false" 81 + > 82 + @IconLeaf() 83 + Bean 84 + </button> 85 + <button 86 + class="dropdown-item flex items-center gap-2 w-full text-left" 87 + hx-get="/api/modals/roaster/new" 88 + hx-target="#modal-container" 89 + hx-swap="innerHTML" 90 + @click="open = false" 91 + > 92 + @IconStore() 93 + Roaster 94 + </button> 95 + // <button 96 + // class="dropdown-item flex items-center gap-2 w-full text-left" 97 + // hx-get="/api/modals/cafe/new" 98 + // hx-target="#modal-container" 99 + // hx-swap="innerHTML" 100 + // @click="open = false" 101 + // > 102 + // @IconMapPin() Cafe 103 + // </button> 104 + <button 105 + class="dropdown-item flex items-center gap-2 w-full text-left" 106 + hx-get="/api/modals/grinder/new" 107 + hx-target="#modal-container" 108 + hx-swap="innerHTML" 109 + @click="open = false" 110 + > 111 + @IconDisc() 112 + Grinder 113 + </button> 114 + <button 115 + class="dropdown-item flex items-center gap-2 w-full text-left" 116 + hx-get="/api/modals/brewer/new" 117 + hx-target="#modal-container" 118 + hx-swap="innerHTML" 119 + @click="open = false" 120 + > 121 + @IconBrewer() 122 + Brewer 123 + </button> 124 + <button 125 + class="dropdown-item flex items-center gap-2 w-full text-left" 126 + hx-get="/api/modals/recipe/new" 127 + hx-target="#modal-container" 128 + hx-swap="innerHTML" 129 + @click="open = false" 130 + > 131 + @IconFileText() 132 + Recipe 133 + </button> 134 + </div> 135 + </div> 48 136 <!-- User profile dropdown --> 49 137 <div x-data="{ open: false }" class="relative"> 50 138 <button @click="open = !open" @click.outside="open = false" class="flex items-center gap-2 hover:opacity-80 transition focus:outline-none">