···1616{"id":"01KGDXVR86CB2H44DJHKN4Z9M0","title":"Implement comments for social interactions","description":"Add a comment lexicon and implement comment functionality across the app.\n\n## Lexicon: social.arabica.alpha.comment\n\nThe comment record should:\n- Use `com.atproto.repo.strongRef` for the subject (URI + CID for immutability)\n- Support commenting on any arabica.social lexicon type (beans, roasters, grinders, brewers, brews, and other comments)\n- Include text field (max 1000 chars / 300 graphemes as per AT Protocol conventions)\n- Include createdAt timestamp\n- Use TID as record key\n- NOTE: This cell implements flat comments only. Threaded/nested comments are handled in a separate cell.\n\n## Backend Implementation\n\n1. Create lexicon file: `lexicons/social.arabica.alpha.comment.json`\n2. Add NSID constant in `internal/atproto/nsid.go`\n3. Add Comment model in `internal/models/models.go`\n4. Add record conversion functions in `internal/atproto/records.go`\n5. Add Store interface methods: CreateComment, DeleteComment, GetCommentsForSubject, GetUserComments\n6. Implement in AtprotoStore\n7. Update firehose indexing to track comments\n\n## Frontend Implementation\n\n1. Add comment section component below brew detail view\n2. Add comment form (authenticated users only)\n3. Display comment count on records in feed\n4. Update comment counts in response to firehose events\n5. Show commenter profile info (avatar, handle)\n\n## Acceptance Criteria\n\n- Users can comment on any arabica.social record\n- Comments are stored in the user's PDS (actor-owned data)\n- Comment counts display on records in the feed\n- Comments visible on record detail views\n- Real-time comment count updates via firehose","status":"open","priority":"normal","labels":["backend","frontend","atproto"],"created_at":"2026-02-02T01:00:51.846427126Z","updated_at":"2026-02-02T01:00:51.846427126Z"}
1717{"id":"01KGDXW31PHFMBE3WTV83HPET1","title":"Implement comment threading","description":"Add support for threaded/nested comments, building on the flat comment system.\n\n## Lexicon Changes\n\nUpdate `social.arabica.alpha.comment` to add optional threading fields:\n- `parent`: Optional strongRef to parent comment (for replies)\n- `root`: Optional strongRef to root subject (maintains context when replying to comments)\n\nAlternatively, consider a separate reply field or keeping comments flat with UI-level threading.\n\n## Backend Implementation\n\n1. Update comment model to include parent/root references\n2. Add methods to fetch comment threads: GetCommentThread, GetReplies\n3. Update firehose indexing to track parent-child relationships\n4. Add depth limits for threading (prevent infinite nesting)\n\n## Frontend Implementation\n\n1. Design threaded comment UI (indentation, collapse/expand)\n2. Add 'reply' button on comments\n3. Show reply context when replying\n4. Consider max nesting depth for display (e.g., 3-4 levels)\n5. Mobile-friendly thread navigation\n\n## Design Considerations\n\n- How deep should threading go? (Recommend max 3-4 levels visible, then flatten)\n- How to handle deleted parent comments?\n- Should users be notified when someone replies to their comment?\n- Performance: lazy-load deep threads vs eager-load\n\n## Acceptance Criteria\n\n- Users can reply directly to comments\n- Thread structure is visually clear\n- Threads can be collapsed/expanded\n- Works well on mobile devices\n- Parent context shown when replying","status":"blocked","priority":"low","blocked_by":["01KGDXVR86CB2H44DJHKN4Z9M0"],"labels":["frontend","atproto"],"created_at":"2026-02-02T01:01:02.902692829Z","updated_at":"2026-02-02T01:01:06.361891137Z"}
1818{"id":"01KJ36P2BWM4HSZV6GNX0BJB1F","title":"Implement lightweight For You feed algorithm","description":"Add a 'For You' algorithmic feed tab alongside the existing chronological feed. This should be a lightweight scoring system that ranks posts based on:\n\n**Scoring Factors:**\n1. **Engagement score**: likes (weight 3x) + comments (weight 2x) on the post\n2. **Time decay**: Score multiplied by a decay factor based on post age. Use exponential decay with a half-life of ~24 hours so recent engaged content surfaces while popular older content still has a chance.\n3. **Type diversity**: After scoring, apply a diversity pass to avoid showing too many of the same record type in a row. If 3+ consecutive items are the same type, interleave with the next different-type item.\n4. **Social proximity** (future): Boost posts from users the viewer has interacted with (liked their content, commented on their posts). This requires building a per-user interaction graph from the like/comment indexes.\n\n**Implementation approach:**\n- Add a new `FeedSortForYou` sort option alongside recent/popular\n- Add a `scoreForYouItem(item *FeedItem, viewerDID string) float64` function in the firehose index\n- Fetch ~100 recent items, score them, apply diversity, return top N\n- Add a 'For You' tab in the feed filter bar UI (only for authenticated users)\n- Cache scored results per-viewer with short TTL (1-2 min) to avoid re-scoring on pagination\n\n**Key files:**\n- `internal/firehose/index.go` - Add scoring logic and ForYou query\n- `internal/feed/service.go` - Add FeedSortForYou constant\n- `internal/firehose/adapter.go` - Pass through ForYou sort\n- `internal/handlers/feed.go` - Handle sort=foryou param\n- `internal/web/pages/feed.templ` - Add For You tab\n\n**Dependencies:**\n- Relies on existing BucketByTime, BucketLikeCounts, BucketCommentCounts, BucketLikesByActor indexes\n- Social proximity scoring depends on being able to query BucketLikesByActor efficiently","status":"open","priority":"normal","created_at":"2026-02-22T17:34:47.67684351Z","updated_at":"2026-02-22T17:34:47.67684351Z"}
1919+{"id":"01KJ37P3RD4X4P7B8F5EWE6NWB","title":"Add Prometheus metrics endpoint","description":"Add prometheus/client_golang instrumentation to Arabica with a /metrics endpoint. Minimal but useful instrumentation points:\n\n## Metrics to implement\n\n### HTTP middleware (internal/middleware/)\n- `arabica_http_requests_total` counter: labels: method, path, status\n- `arabica_http_request_duration_seconds` histogram: labels: method, path\n\n### Firehose consumer (internal/firehose/)\n- `arabica_firehose_events_total` counter: labels: collection, operation\n- `arabica_firehose_connection_state` gauge: 1=connected, 0=disconnected\n- `arabica_firehose_errors_total` counter\n\n### PDS client (internal/atproto/)\n- `arabica_pds_requests_total` counter: labels: method, collection\n- `arabica_pds_request_duration_seconds` histogram: labels: method\n- `arabica_pds_errors_total` counter: labels: method\n\n### Feed service (internal/feed/)\n- `arabica_feed_cache_hits_total` counter\n- `arabica_feed_cache_misses_total` counter\n\n## Implementation notes\n- Add `github.com/prometheus/client_golang` dependency\n- Expose /metrics endpoint in routing.go (no auth required)\n- Keep cardinality low: normalize path labels (e.g. /brews/{id} -\u003e /brews/:id)\n- Create a grafana/arabica-prometheus.json dashboard alongside the existing log-based one\n- Existing log-based dashboard is at grafana/arabica-logs.json for reference","status":"completed","priority":"normal","assignee":"patrick","labels":["backend","observability"],"created_at":"2026-02-22T17:52:17.677417112Z","updated_at":"2026-02-22T17:57:56.326244513Z","completed_at":"2026-02-22T17:57:56.315753758Z"}
+1
CLAUDE.md
···450450| `ARABICA_FEED_INDEX_PATH` | ~/.local/share/arabica/feed-index.db | Firehose index BoltDB path |
451451| `ARABICA_MODERATORS_CONFIG` | - | Path to moderators JSON config |
452452| `ARABICA_PROFILE_CACHE_TTL` | 1h | Profile cache duration |
453453+| `METRICS_PORT` | 9101 | Internal metrics server port (localhost only) |
453454| `SECURE_COOKIES` | false | Set true for HTTPS |
454455| `LOG_LEVEL` | info | debug/info/warn/error |
455456| `LOG_FORMAT` | console | console/json |
+45-1
cmd/server/main.go
···2020 "arabica/internal/feed"
2121 "arabica/internal/firehose"
2222 "arabica/internal/handlers"
2323+ "arabica/internal/metrics"
2324 "arabica/internal/moderation"
2425 "arabica/internal/routing"
25262727+ "github.com/prometheus/client_golang/prometheus/promhttp"
2628 "github.com/rs/zerolog"
2729 "github.com/rs/zerolog/log"
2830)
···193195194196 log.Info().Msg("Firehose consumer started")
195197198198+ // Start metrics collector for periodic gauge updates
199199+ metrics.StartCollector(ctx, metrics.StatsSource{
200200+ KnownDIDCount: feedIndex.KnownDIDCount,
201201+ RegisteredCount: feedRegistry.Count,
202202+ RecordCount: feedIndex.RecordCount,
203203+ PendingJoinCount: func() int {
204204+ joinStore := store.JoinStore()
205205+ if reqs, err := joinStore.ListRequests(); err == nil {
206206+ return len(reqs)
207207+ }
208208+ return 0
209209+ },
210210+ LikeCount: feedIndex.TotalLikeCount,
211211+ CommentCount: feedIndex.TotalCommentCount,
212212+ RecordCountByCollection: feedIndex.RecordCountByCollection,
213213+ FirehoseConnected: firehoseConsumer.IsConnected,
214214+ }, 60*time.Second)
215215+196216 // Log known DIDs from database (DIDs discovered via firehose)
197217 if knownDIDsFromDB, err := feedIndex.GetKnownDIDs(); err == nil {
198218 if len(knownDIDsFromDB) > 0 {
···344364 Logger: log.Logger,
345365 })
346366367367+ // Start internal metrics server on localhost only (not publicly accessible)
368368+ metricsPort := os.Getenv("METRICS_PORT")
369369+ if metricsPort == "" {
370370+ metricsPort = "9101"
371371+ }
372372+ metricsMux := http.NewServeMux()
373373+ metricsMux.Handle("GET /metrics", promhttp.Handler())
374374+ metricsServer := &http.Server{
375375+ Addr: "127.0.0.1:" + metricsPort,
376376+ Handler: metricsMux,
377377+ }
378378+ go func() {
379379+ log.Info().
380380+ Str("address", "127.0.0.1:"+metricsPort).
381381+ Msg("Starting metrics server (localhost only)")
382382+ if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
383383+ log.Error().Err(err).Msg("Metrics server failed to start")
384384+ }
385385+ }()
386386+347387 // Create HTTP server
348388 server := &http.Server{
349389 Addr: "0.0.0.0:" + port,
···372412 log.Info().Msg("Stopping firehose consumer...")
373413 firehoseConsumer.Stop()
374414375375- // Graceful shutdown of HTTP server
415415+ // Graceful shutdown of HTTP server and metrics server
376416 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
377417 defer shutdownCancel()
418418+419419+ if err := metricsServer.Shutdown(shutdownCtx); err != nil {
420420+ log.Error().Err(err).Msg("Metrics server shutdown error")
421421+ }
378422379423 if err := server.Shutdown(shutdownCtx); err != nil {
380424 log.Error().Err(err).Msg("HTTP server shutdown error")
···11+# Grafana Dashboards
22+33+Importable Grafana dashboard definitions for monitoring Arabica.
44+55+## Dashboards
66+77+### `arabica-logs.json` - Log-Based Metrics
88+99+Queries structured JSON logs via **Loki**. No code changes needed - works with existing zerolog output.
1010+1111+**Prerequisite:** Ship Arabica logs to Loki (e.g., via Promtail, Alloy, or Docker log driver). Logs must be in JSON format (`LOG_FORMAT=json`).
1212+1313+**Log selector:** The dashboard uses a template variable (`$log_selector`) with three presets:
1414+1515+- `unit="arabica.service"` (default) - NixOS/systemd journal via Promtail
1616+- `syslog_identifier="arabica"` - journald syslog identifier
1717+- `app="arabica"` - Docker log driver or custom labels
1818+1919+Select the matching option from the dropdown at the top of the dashboard, or type a custom value.
2020+2121+**Sections:**
2222+2323+- **Overview** - stat panels for total requests, errors, logins, reports, join requests
2424+- **HTTP Traffic** - requests by status/method, top paths, response latency
2525+- **Firehose** - events by collection/operation, errors, backfill activity
2626+- **Authentication & Users** - login success/failure, join requests, invites
2727+- **Moderation** - reports, hide/unhide/block actions, permission denials
2828+- **PDS & ATProto** - PDS request volume/latency/errors by method and collection
2929+- **Errors & Warnings** - error/warn timeline + recent error log viewer
3030+3131+### `arabica-prometheus.json` - Prometheus Metrics
3232+3333+Queries instrumented Prometheus counters, histograms, and gauges exposed at `/metrics`.
3434+3535+**Prerequisite:** Arabica exposes a `/metrics` endpoint (Prometheus format). Configure Prometheus to scrape it.
3636+3737+**Sections:**
3838+3939+- **Overview** - request rate, error rate, p95 latency, firehose connection, events/s, cache hit rate
4040+- **HTTP Traffic** - request rate by status/path, latency percentiles (p50/p95/p99), latency by path
4141+- **Firehose** - events by collection/operation, error rate, connection state
4242+- **PDS / ATProto** - PDS request rate by method/collection, latency by method, error rate
4343+- **Feed Cache** - cache hits vs misses, hit rate over time
4444+4545+### Importing
4646+4747+1. In Grafana, go to **Dashboards > Import**
4848+2. Upload the JSON file or paste its contents
4949+3. Select your data source (Loki or Prometheus) when prompted
5050+4. For the Loki dashboard, select the correct log selector from the dropdown (defaults to `unit="arabica.service"` for NixOS systemd)