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

fix: action menu divider fix

pdewey.com c8aefabd d8237f1a

verified
+14 -5
+8
.cells/cells.jsonl
··· 50 {"id":"01KGGK6V4WMC3X67E72JTE27A7","title":"Fix 'failed to convert record to feed item' warning for likes","description":"The firehose index logs a warning 'failed to convert record to feed item' when processing like records. This is expected behavior since likes are not displayed as standalone feed items - they're indexed for like counts but shouldn't appear in the feed.\n\nThe warning appears in internal/firehose/index.go:433 when recordToFeedItem returns an error for NSIDLike records (line 582-584).\n\nFix: Handle the like case silently without logging a warning, since this is expected behavior. Either:\n1. Skip like records before calling recordToFeedItem\n2. Return a special sentinel error from recordToFeedItem that indicates 'skip without warning'\n3. Check the collection type at the call site and skip likes there\n\nThis applies to both authenticated and unauthenticated users.","status":"completed","priority":"normal","labels":["bug"],"created_at":"2026-02-03T01:52:24.220349056Z","updated_at":"2026-02-03T01:54:54.698106017Z","completed_at":"2026-02-03T01:54:54.686934901Z"} 51 {"id":"01KGGNPG25M2AJ619P65FPMEH4","title":"Add OpenGraph metadata to brew pages","description":"Add dynamic OpenGraph metadata support for brew pages to enable rich social sharing previews.\n\n## Background\n\nCurrently, the site uses static OpenGraph tags in layout.templ:\n- og:title: \"Arabica - Coffee Brew Tracker\" (static)\n- og:description: \"Track your coffee brewing journey...\" (static) \n- og:type: \"website\" (static)\n- og:image: NOT PRESENT\n\nWhen users share brew links on social media, all previews look identical and provide no context about the specific brew.\n\n## Requirements\n\n### 1. Extend LayoutData struct\n\nAdd optional OpenGraph fields to `internal/web/components/layout.templ`:\n\n```go\ntype LayoutData struct {\n Title string\n IsAuthenticated bool\n UserDID string\n UserProfile *bff.UserProfile\n CSPNonce string\n \n // OpenGraph metadata (optional, falls back to defaults)\n OGTitle string\n OGDescription string\n OGImage string\n OGType string // \"website\", \"article\"\n OGUrl string // Canonical URL\n}\n```\n\n### 2. Update layout.templ head section\n\nModify meta tag rendering to use dynamic values with fallbacks:\n- If OGTitle set, use it; otherwise use site default\n- If OGDescription set, use it; otherwise use site default\n- If OGImage set, render og:image tag (currently missing entirely)\n- If OGUrl set, render og:url tag\n- Support og:type (default \"website\", brew pages use \"article\")\n\n### 3. Update buildLayoutData helper\n\nExtend `handlers.go` buildLayoutData() to accept optional OG parameters, or create a new helper `buildLayoutDataWithOG()`.\n\n### 4. Update HandleBrewView handler\n\nConstruct descriptive OG metadata from brew data:\n- **og:title**: \"{Bean Name} from {Origin} - Arabica\" or similar\n- **og:description**: \"{Rating}/10 - {Tasting Notes preview}\" or brew summary\n- **og:type**: \"article\"\n- **og:url**: The share URL (already computed as `shareURL`)\n- **og:image**: Static default for now (e.g., /static/og-brew-default.png)\n\nAvailable data for description construction:\n- brew.Rating, brew.TastingNotes\n- brew.Bean.Name, brew.Bean.Origin, brew.Bean.RoastLevel\n- brew.Bean.Roaster.Name\n- brew.CoffeeAmount, brew.WaterAmount, brew.Temperature\n\n### 5. Add Twitter Card support\n\nAdd Twitter-specific meta tags:\n- twitter:card = \"summary\" (or \"summary_large_image\" if we have images)\n- twitter:title, twitter:description (same as OG values)\n\n### 6. Static fallback image\n\nCreate or source a default OG image for brews:\n- Location: /static/og-brew-default.png or similar\n- Size: 1200x630px (recommended OG image size)\n- Design: Coffee-themed, includes Arabica branding\n\n## Acceptance Criteria\n\n- [ ] LayoutData extended with optional OG fields\n- [ ] layout.templ renders dynamic OG tags with fallbacks\n- [ ] Brew view pages include descriptive OG metadata\n- [ ] Twitter Card tags included\n- [ ] Static default OG image created and served\n- [ ] Existing pages (home, about, etc.) continue working with defaults\n- [ ] templ generate runs successfully\n- [ ] Manual testing: share brew URL to social platform preview tool\n\n## Implementation Notes\n\n- Follow existing patterns in handlers.go for data flow\n- Keep backwards compatible - pages not setting OG fields should use defaults\n- Consider creating a helper function to build OG description from brew data\n- The shareURL is already constructed in HandleBrewView (lines 695-701)\n\n## Future Enhancements (out of scope)\n\n- Dynamic image generation showing brew stats\n- Profile page OG metadata\n- Feed page OG metadata with recent brew preview","status":"completed","priority":"normal","assignee":"patrick","labels":["frontend","social"],"created_at":"2026-02-03T02:35:54.309356038Z","updated_at":"2026-02-03T02:53:43.170537883Z","completed_at":"2026-02-03T02:53:43.15817599Z","notes":[{"timestamp":"2026-02-03T02:53:36.100130152Z","author":"patrick","message":"Implementation complete:\n- Extended LayoutData with OG fields (OGTitle, OGDescription, OGImage, OGType, OGUrl)\n- Added helper methods ogTitle(), ogDescription(), ogType() with fallbacks\n- Updated layout.templ to render dynamic OG and Twitter Card meta tags\n- Added populateBrewOGMetadata() helper in handlers.go\n- Updated HandleBrewView to populate OG metadata from brew data\n- Added PublicURL to handler Config for absolute URLs\n- Added comprehensive tests for OG metadata generation\n- All tests pass"}]} 52 {"id":"01KGM9QB86N5RX50042DT1YN1N","title":"Consolidate Tailwind CSS usage","description":"Reduce Tailwind class duplication by leveraging existing CSS abstractions and adding missing utility classes.\n\n## Changes Required\n\n### 1. Add new utility classes to app.css\n- `.page-container` variants (sm, md, lg, xl) for max-width containers\n- `.avatar-sm`, `.avatar-md`, `.avatar-lg` size classes\n- `.section-box` for bg-brown-50 rounded sections\n\n### 2. Update templ components to use existing abstractions\n- WelcomeCard (shared.templ) - use `.card` instead of inline gradient\n- ProfileHeader, ProfileStat, ProfileTabs (profile.templ) - use `.card`\n- Header dropdown (header.templ) - use `.action-menu`\n- Login/CTA buttons (shared.templ) - use `.btn-primary`\n- Avatar component (shared.templ) - use new CSS classes instead of templ.KV\n\n### 3. Replace inline page containers\n- Replace `max-w-4xl mx-auto` etc. with `.page-container-*` classes across all pages\n\n## Acceptance Criteria\n- No visual changes to the site\n- Reduced class verbosity in templ files\n- Avatar component simplified\n- Consistent use of existing abstractions","status":"completed","priority":"normal","assignee":"patrick","labels":["css","refactor"],"created_at":"2026-02-04T12:23:36.966151046Z","updated_at":"2026-02-04T12:27:59.86186814Z","completed_at":"2026-02-04T12:27:59.850499942Z"}
··· 50 {"id":"01KGGK6V4WMC3X67E72JTE27A7","title":"Fix 'failed to convert record to feed item' warning for likes","description":"The firehose index logs a warning 'failed to convert record to feed item' when processing like records. This is expected behavior since likes are not displayed as standalone feed items - they're indexed for like counts but shouldn't appear in the feed.\n\nThe warning appears in internal/firehose/index.go:433 when recordToFeedItem returns an error for NSIDLike records (line 582-584).\n\nFix: Handle the like case silently without logging a warning, since this is expected behavior. Either:\n1. Skip like records before calling recordToFeedItem\n2. Return a special sentinel error from recordToFeedItem that indicates 'skip without warning'\n3. Check the collection type at the call site and skip likes there\n\nThis applies to both authenticated and unauthenticated users.","status":"completed","priority":"normal","labels":["bug"],"created_at":"2026-02-03T01:52:24.220349056Z","updated_at":"2026-02-03T01:54:54.698106017Z","completed_at":"2026-02-03T01:54:54.686934901Z"} 51 {"id":"01KGGNPG25M2AJ619P65FPMEH4","title":"Add OpenGraph metadata to brew pages","description":"Add dynamic OpenGraph metadata support for brew pages to enable rich social sharing previews.\n\n## Background\n\nCurrently, the site uses static OpenGraph tags in layout.templ:\n- og:title: \"Arabica - Coffee Brew Tracker\" (static)\n- og:description: \"Track your coffee brewing journey...\" (static) \n- og:type: \"website\" (static)\n- og:image: NOT PRESENT\n\nWhen users share brew links on social media, all previews look identical and provide no context about the specific brew.\n\n## Requirements\n\n### 1. Extend LayoutData struct\n\nAdd optional OpenGraph fields to `internal/web/components/layout.templ`:\n\n```go\ntype LayoutData struct {\n Title string\n IsAuthenticated bool\n UserDID string\n UserProfile *bff.UserProfile\n CSPNonce string\n \n // OpenGraph metadata (optional, falls back to defaults)\n OGTitle string\n OGDescription string\n OGImage string\n OGType string // \"website\", \"article\"\n OGUrl string // Canonical URL\n}\n```\n\n### 2. Update layout.templ head section\n\nModify meta tag rendering to use dynamic values with fallbacks:\n- If OGTitle set, use it; otherwise use site default\n- If OGDescription set, use it; otherwise use site default\n- If OGImage set, render og:image tag (currently missing entirely)\n- If OGUrl set, render og:url tag\n- Support og:type (default \"website\", brew pages use \"article\")\n\n### 3. Update buildLayoutData helper\n\nExtend `handlers.go` buildLayoutData() to accept optional OG parameters, or create a new helper `buildLayoutDataWithOG()`.\n\n### 4. Update HandleBrewView handler\n\nConstruct descriptive OG metadata from brew data:\n- **og:title**: \"{Bean Name} from {Origin} - Arabica\" or similar\n- **og:description**: \"{Rating}/10 - {Tasting Notes preview}\" or brew summary\n- **og:type**: \"article\"\n- **og:url**: The share URL (already computed as `shareURL`)\n- **og:image**: Static default for now (e.g., /static/og-brew-default.png)\n\nAvailable data for description construction:\n- brew.Rating, brew.TastingNotes\n- brew.Bean.Name, brew.Bean.Origin, brew.Bean.RoastLevel\n- brew.Bean.Roaster.Name\n- brew.CoffeeAmount, brew.WaterAmount, brew.Temperature\n\n### 5. Add Twitter Card support\n\nAdd Twitter-specific meta tags:\n- twitter:card = \"summary\" (or \"summary_large_image\" if we have images)\n- twitter:title, twitter:description (same as OG values)\n\n### 6. Static fallback image\n\nCreate or source a default OG image for brews:\n- Location: /static/og-brew-default.png or similar\n- Size: 1200x630px (recommended OG image size)\n- Design: Coffee-themed, includes Arabica branding\n\n## Acceptance Criteria\n\n- [ ] LayoutData extended with optional OG fields\n- [ ] layout.templ renders dynamic OG tags with fallbacks\n- [ ] Brew view pages include descriptive OG metadata\n- [ ] Twitter Card tags included\n- [ ] Static default OG image created and served\n- [ ] Existing pages (home, about, etc.) continue working with defaults\n- [ ] templ generate runs successfully\n- [ ] Manual testing: share brew URL to social platform preview tool\n\n## Implementation Notes\n\n- Follow existing patterns in handlers.go for data flow\n- Keep backwards compatible - pages not setting OG fields should use defaults\n- Consider creating a helper function to build OG description from brew data\n- The shareURL is already constructed in HandleBrewView (lines 695-701)\n\n## Future Enhancements (out of scope)\n\n- Dynamic image generation showing brew stats\n- Profile page OG metadata\n- Feed page OG metadata with recent brew preview","status":"completed","priority":"normal","assignee":"patrick","labels":["frontend","social"],"created_at":"2026-02-03T02:35:54.309356038Z","updated_at":"2026-02-03T02:53:43.170537883Z","completed_at":"2026-02-03T02:53:43.15817599Z","notes":[{"timestamp":"2026-02-03T02:53:36.100130152Z","author":"patrick","message":"Implementation complete:\n- Extended LayoutData with OG fields (OGTitle, OGDescription, OGImage, OGType, OGUrl)\n- Added helper methods ogTitle(), ogDescription(), ogType() with fallbacks\n- Updated layout.templ to render dynamic OG and Twitter Card meta tags\n- Added populateBrewOGMetadata() helper in handlers.go\n- Updated HandleBrewView to populate OG metadata from brew data\n- Added PublicURL to handler Config for absolute URLs\n- Added comprehensive tests for OG metadata generation\n- All tests pass"}]} 52 {"id":"01KGM9QB86N5RX50042DT1YN1N","title":"Consolidate Tailwind CSS usage","description":"Reduce Tailwind class duplication by leveraging existing CSS abstractions and adding missing utility classes.\n\n## Changes Required\n\n### 1. Add new utility classes to app.css\n- `.page-container` variants (sm, md, lg, xl) for max-width containers\n- `.avatar-sm`, `.avatar-md`, `.avatar-lg` size classes\n- `.section-box` for bg-brown-50 rounded sections\n\n### 2. Update templ components to use existing abstractions\n- WelcomeCard (shared.templ) - use `.card` instead of inline gradient\n- ProfileHeader, ProfileStat, ProfileTabs (profile.templ) - use `.card`\n- Header dropdown (header.templ) - use `.action-menu`\n- Login/CTA buttons (shared.templ) - use `.btn-primary`\n- Avatar component (shared.templ) - use new CSS classes instead of templ.KV\n\n### 3. Replace inline page containers\n- Replace `max-w-4xl mx-auto` etc. with `.page-container-*` classes across all pages\n\n## Acceptance Criteria\n- No visual changes to the site\n- Reduced class verbosity in templ files\n- Avatar component simplified\n- Consistent use of existing abstractions","status":"completed","priority":"normal","assignee":"patrick","labels":["css","refactor"],"created_at":"2026-02-04T12:23:36.966151046Z","updated_at":"2026-02-04T12:27:59.86186814Z","completed_at":"2026-02-04T12:27:59.850499942Z"} 53 + \\\\\\\ to: kxrlzxyu 98a353d2 "fix: action menu divider fix" (rebased revision) 54 + {"id":"01KGK913NFHX2MMAXHCXT5S8HM","title":"Implement moderation service core with role-based permissions","description":"Create the core moderation service with JSON-based role configuration.\n\n## Requirements\n\n### JSON Config File (config/moderators.json)\n```json\n{\n \"roles\": {\n \"admin\": {\n \"description\": \"Full platform control\",\n \"permissions\": [\"hide_record\", \"unhide_record\", \"blacklist_user\", \"unblacklist_user\", \"view_reports\", \"dismiss_report\", \"view_audit_log\"]\n },\n \"moderator\": {\n \"description\": \"Content moderation\",\n \"permissions\": [\"hide_record\", \"unhide_record\", \"view_reports\", \"dismiss_report\"]\n }\n },\n \"users\": [\n {\"did\": \"did:plc:xxx\", \"handle\": \"arabica.social\", \"role\": \"admin\", \"note\": \"Platform owner\"}\n ]\n}\n```\n\n### ModerationService (internal/moderation/service.go)\n- Load config from JSON file (path from env var or flag)\n- `IsAdmin(did string) bool`\n- `IsModerator(did string) bool` (includes admins)\n- `HasPermission(did string, permission string) bool`\n- `GetRole(did string) (Role, bool)`\n- `ListModerators() []ModeratorUser`\n\n### Models (internal/moderation/models.go)\n- Role struct with permissions\n- ModeratorUser struct\n- Config struct\n\nAcceptance criteria:\n- Config file is validated on load (unknown roles, missing required fields)\n- Service is nil-safe (returns false/empty if not configured)\n- Reload capability for config changes without restart (nice to have)\n- Unit tests for permission checking","status":"completed","priority":"high","assignee":"patrick","labels":["moderation","backend"],"created_at":"2026-02-04T02:52:13.871235963Z","updated_at":"2026-02-04T03:07:38.338966523Z","completed_at":"2026-02-04T03:07:38.323669324Z","notes":[{"timestamp":"2026-02-04T03:07:38.24039103Z","author":"patrick","message":"Implemented core moderation service:\n- Created internal/moderation/models.go with Role, Permission, ModeratorUser, and Config types\n- Created internal/moderation/service.go with role-based permission checking\n- Added 17 unit tests covering all functionality\n- Created config/moderators.json.example with example configuration\n- Updated CLAUDE.md with new env var ARABICA_MODERATORS_CONFIG and project structure"}]} 55 + {"id":"01KGK913QKZ96RKRH37VB99C7D","title":"Implement moderation storage in BoltDB","description":"Add BoltDB buckets and operations for moderation data.\n\n## Storage Requirements\n\n### Buckets\n- `moderation_hidden_records` - AT-URIs of hidden records\n- `moderation_blacklisted_users` - Blacklisted DIDs with metadata\n- `moderation_reports` - User reports on content\n- `moderation_audit_log` - All moderation actions\n\n### Models (internal/moderation/models.go additions)\n\n```go\ntype HiddenRecord struct {\n ATURI string `json:\"at_uri\"`\n HiddenAt time.Time `json:\"hidden_at\"`\n HiddenBy string `json:\"hidden_by\"` // DID of moderator\n Reason string `json:\"reason\"`\n AutoHidden bool `json:\"auto_hidden\"` // true if automod\n}\n\ntype BlacklistedUser struct {\n DID string `json:\"did\"`\n BlacklistedAt time.Time `json:\"blacklisted_at\"`\n BlacklistedBy string `json:\"blacklisted_by\"`\n Reason string `json:\"reason\"`\n}\n\ntype Report struct {\n ID string `json:\"id\"` // TID\n SubjectURI string `json:\"subject_uri\"` // AT-URI of reported content\n SubjectDID string `json:\"subject_did\"` // DID of content owner\n ReporterDID string `json:\"reporter_did\"`\n Reason string `json:\"reason\"`\n CreatedAt time.Time `json:\"created_at\"`\n Status string `json:\"status\"` // pending, dismissed, actioned\n ResolvedBy string `json:\"resolved_by\"`\n ResolvedAt *time.Time `json:\"resolved_at\"`\n}\n\ntype AuditEntry struct {\n ID string `json:\"id\"`\n Action string `json:\"action\"` // hide_record, blacklist_user, etc.\n ActorDID string `json:\"actor_did\"`\n TargetURI string `json:\"target_uri\"` // AT-URI or DID\n Reason string `json:\"reason\"`\n Timestamp time.Time `json:\"timestamp\"`\n AutoMod bool `json:\"auto_mod\"`\n}\n```\n\n### Store Interface (internal/moderation/store.go)\n```go\ntype Store interface {\n // Hidden records\n HideRecord(ctx context.Context, entry HiddenRecord) error\n UnhideRecord(ctx context.Context, atURI string) error\n IsRecordHidden(ctx context.Context, atURI string) bool\n ListHiddenRecords(ctx context.Context) ([]HiddenRecord, error)\n \n // Blacklist\n BlacklistUser(ctx context.Context, entry BlacklistedUser) error\n UnblacklistUser(ctx context.Context, did string) error\n IsBlacklisted(ctx context.Context, did string) bool\n ListBlacklistedUsers(ctx context.Context) ([]BlacklistedUser, error)\n \n // Reports\n CreateReport(ctx context.Context, report Report) error\n GetReport(ctx context.Context, id string) (*Report, error)\n ListPendingReports(ctx context.Context) ([]Report, error)\n ResolveReport(ctx context.Context, id string, status string, resolvedBy string) error\n CountReportsForURI(ctx context.Context, atURI string) (int, error)\n CountReportsForDID(ctx context.Context, did string) (int, error)\n \n // Audit\n LogAction(ctx context.Context, entry AuditEntry) error\n ListAuditLog(ctx context.Context, limit int) ([]AuditEntry, error)\n}\n```\n\nAcceptance criteria:\n- All operations are atomic\n- Audit logging happens automatically on moderation actions\n- Efficient lookups for IsRecordHidden and IsBlacklisted (called on every feed item)\n- Unit tests for store operations","status":"completed","priority":"high","assignee":"patrick","labels":["moderation","backend","database"],"created_at":"2026-02-04T02:52:13.939164045Z","updated_at":"2026-02-04T03:12:29.407349995Z","completed_at":"2026-02-04T03:12:29.395926199Z","notes":[{"timestamp":"2026-02-04T03:12:29.301269019Z","author":"patrick","message":"Implemented BoltDB storage for moderation:\n- Added 6 new buckets for moderation data in store.go\n- Created moderation_store.go with full CRUD operations:\n - Hidden records: HideRecord, UnhideRecord, IsRecordHidden, GetHiddenRecord, ListHiddenRecords\n - Blacklist: BlacklistUser, UnblacklistUser, IsBlacklisted, GetBlacklistedUser, ListBlacklistedUsers\n - Reports: CreateReport, GetReport, ListPendingReports, ResolveReport, CountReportsForURI/DID, HasReportedURI\n - Audit: LogAction, ListAuditLog\n- Added indexes for efficient report counting by URI and DID\n- Created comprehensive tests (20 test cases)"}]} 56 + {"id":"01KGK91R3NWQEPPSSK9035JFZQ","title":"Integrate moderation filtering into feed service","description":"Hook moderation service into feed aggregation to filter hidden content and blacklisted users.\n\n## Requirements\n\n### Feed Service Changes (internal/feed/service.go)\n- Accept ModerationService dependency\n- Filter out hidden records before returning feed items\n- Filter out all records from blacklisted users\n- Consider performance: batch lookups vs per-item checks\n\n### Integration Points\n```go\ntype FeedService struct {\n // existing fields...\n moderation *moderation.Service\n}\n\nfunc (f *FeedService) GetFeed(ctx context.Context) ([]FeedItem, error) {\n items := f.getRawFeed(ctx)\n return f.filterModerated(ctx, items), nil\n}\n\nfunc (f *FeedService) filterModerated(ctx context.Context, items []FeedItem) []FeedItem {\n var filtered []FeedItem\n for _, item := range items {\n if f.moderation.IsBlacklisted(ctx, item.AuthorDID) {\n continue\n }\n if f.moderation.IsRecordHidden(ctx, item.ATURI) {\n continue\n }\n filtered = append(filtered, item)\n }\n return filtered\n}\n```\n\n### Public Brew View\n- Also filter on individual brew view endpoint (/brews/{did}/{rkey})\n- Return 404 for hidden records (or 451 Unavailable For Legal Reasons?)\n- Return 404 for blacklisted users' content\n\n### Handler Updates (internal/handlers/handlers.go)\n- Inject moderation service\n- Check moderation status on brew view routes\n\nAcceptance criteria:\n- Hidden records don't appear in feed\n- Blacklisted users' content doesn't appear anywhere\n- Minimal performance impact (consider caching blacklist/hidden sets)\n- Graceful degradation if moderation service unavailable","status":"completed","priority":"high","assignee":"patrick","labels":["moderation","backend","feed"],"created_at":"2026-02-04T02:52:34.805956493Z","updated_at":"2026-02-04T12:45:54.18942141Z","completed_at":"2026-02-04T12:45:54.173769314Z"} 57 + {"id":"01KGK91R5RCE0B0PASS301ZZQT","title":"Implement user report submission system","description":"Allow authenticated users to report content with basic automod thresholds.\n\n## Requirements\n\n### Report Endpoint\n- POST /api/reports\n- Requires authentication\n- Rate limited (max 10 reports per user per hour)\n\n### Request/Response\n```go\ntype ReportRequest struct {\n SubjectURI string `json:\"subject_uri\"` // AT-URI of content\n Reason string `json:\"reason\"` // free text, max 500 chars\n}\n\ntype ReportResponse struct {\n ID string `json:\"id\"`\n Message string `json:\"message\"`\n}\n```\n\n### Automod Thresholds\nConfigurable via JSON config or constants:\n```go\nconst (\n AutoHideThreshold = 3 // reports on single record\n AutoHideUserThreshold = 5 // total reports across user's records\n NewUserDays = 7 // users \u003c 7 days old\n NewUserAutoHideThreshold = 2 // lower threshold for new users\n)\n```\n\nWhen threshold reached:\n1. Auto-hide the record (mark AutoHidden: true)\n2. Create audit log entry with AutoMod: true\n3. Keep report in pending state for moderator review\n\n### UI Component\n- Report button on brew cards in feed\n- Report button on brew detail page\n- Simple modal: textarea for reason + submit\n- Confirmation message after submission\n\n### Handler (internal/handlers/moderation.go)\n```go\nfunc (h *Handler) HandleSubmitReport(w http.ResponseWriter, r *http.Request)\n```\n\nAcceptance criteria:\n- Users can report any content (not their own)\n- Duplicate reports from same user are rejected\n- Automod triggers when thresholds reached\n- Rate limiting prevents abuse\n- Success/error feedback to user","status":"open","priority":"high","labels":["moderation","backend","frontend"],"created_at":"2026-02-04T02:52:34.872474383Z","updated_at":"2026-02-04T03:12:29.413298406Z"} 58 + {"id":"01KGK926RAW8Q3SQHFH4KKKM3K","title":"Implement admin/moderator dashboard UI","description":"Create the admin dashboard for moderators and admins to take moderation actions.\n\n## Requirements\n\n### Routes\n- GET /admin - Dashboard (moderators + admins)\n- GET /admin/reports - Pending reports view\n- GET /admin/audit - Audit log (admins only)\n- POST /admin/hide - Hide a record\n- POST /admin/unhide - Unhide a record\n- POST /admin/blacklist - Blacklist a user (admins only)\n- POST /admin/unblacklist - Unblacklist a user (admins only)\n- POST /admin/reports/{id}/resolve - Resolve a report\n\n### Access Control\n- All /admin routes require authentication\n- Check HasPermission() for each action\n- Return 403 if insufficient permissions\n- Hide UI elements user can't access\n\n### Dashboard Page (internal/web/pages/admin.templ)\n```\n┌─────────────────────────────────────────────────────┐\n│ Moderation Dashboard [Your Role] │\n├─────────────────────────────────────────────────────┤\n│ │\n│ Pending Reports (3) │\n│ ┌─────────────────────────────────────────────────┐ │\n│ │ @user reported brew by @other │ │\n│ │ \"Spam content\" [View] [Hide] [Dismiss]│ │\n│ └─────────────────────────────────────────────────┘ │\n│ │\n│ Recently Hidden (auto-hidden marked) │\n│ ┌─────────────────────────────────────────────────┐ │\n│ │ brew/abc by @user [AUTO] - 3 reports [Unhide] │ │\n│ └─────────────────────────────────────────────────┘ │\n│ │\n│ Blacklisted Users (admin only) │\n│ ┌─────────────────────────────────────────────────┐ │\n│ │ @baduser - \"repeated spam\" [Unblacklist] │ │\n│ └─────────────────────────────────────────────────┘ │\n│ │\n│ [View Audit Log] (admin only) │\n└─────────────────────────────────────────────────────┘\n```\n\n### Components\n- AdminLayout component (reuses main layout but adds admin nav)\n- ReportCard component\n- HiddenRecordRow component\n- BlacklistedUserRow component\n- AuditLogTable component\n\n### HTMX Integration\n- Actions (hide/unhide/resolve) via HTMX POST\n- Optimistic UI updates\n- Error toast on failure\n\n### Handler (internal/handlers/admin.go)\nNew handler file for admin routes, or add to existing handlers.go\n\nAcceptance criteria:\n- Only accessible to moderators/admins\n- All actions require appropriate permissions\n- Actions create audit log entries\n- Responsive design\n- Confirmation dialogs for destructive actions (blacklist)","status":"completed","priority":"high","assignee":"patrick","blocked_by":["01KGK91R3NWQEPPSSK9035JFZQ","01KGK91R5RCE0B0PASS301ZZQT"],"labels":["moderation","frontend","backend"],"created_at":"2026-02-04T02:52:49.80269115Z","updated_at":"2026-02-05T02:05:48.756110333Z","completed_at":"2026-02-05T02:05:48.744596981Z"} 59 + {"id":"01KGMAQBRS5J9N0JHBJAAZ518Q","title":"Add moderator actions to feed item more menu","description":"Add moderation actions (hide/unhide) to the existing 'more' dropdown menu on feed items for users with appropriate permissions.\n\n## Requirements\n\n### UI Changes\n- Extend the existing 'more' menu (three dots) on feed/brew cards\n- Show moderation actions only if user has permission\n- Actions to add:\n - 'Hide from feed' (if not hidden, requires hide_record permission)\n - 'Unhide' (if hidden, requires unhide_record permission)\n - Visual indicator if record is currently hidden (for mods viewing hidden content)\n\n### Backend Changes\n- Pass moderation context to feed/brew templates:\n - `IsModerator bool`\n - `CanHideRecord bool`\n - `IsRecordHidden bool` (per item)\n- Add HTMX endpoints:\n - POST /admin/hide (accepts AT-URI, returns updated menu)\n - POST /admin/unhide (accepts AT-URI, returns updated menu)\n\n### Integration Points\n- Feed page (feed.templ) - brew cards in community feed\n- Profile page (profile.templ) - brew cards on user profiles\n- Brew view page (brew_view.templ) - single brew detail view\n\n### Behavior\n- Actions trigger HTMX POST requests\n- On success: swap the menu to show updated state\n- On error: show toast notification\n- Create audit log entry for each action\n\n### Permissions Check\n- Check ModerationService.HasPermission() before rendering actions\n- Check again server-side before executing action\n- Return 403 if permission denied\n\nAcceptance criteria:\n- Moderators see hide/unhide options in more menu\n- Regular users don't see moderation options\n- Actions work via HTMX without full page reload\n- Audit log entries created for all actions\n- Hidden records show visual indicator to moderators","status":"completed","priority":"high","assignee":"patrick","labels":["moderation","frontend","backend"],"created_at":"2026-02-04T12:41:06.073520449Z","updated_at":"2026-02-04T13:06:16.292634304Z","completed_at":"2026-02-04T13:06:16.279224398Z"} 60 + +{"id":"01KGZQ1Q1CSCBNXY9AS6GQJK2B","title":"Fix more actions menu z-index on feed cards","description":"The action bar border-t divider line visually paints on top of the popup menu instead of behind it. Root cause: .action-menu has z-10 while .dropdown-menu has z-50, and .feed-card animation creates a new stacking context.\n\nFix:\n- Bump .action-menu z-index from z-10 to z-50 (matching .dropdown-menu) in static/css/app.css\n- Add isolation: isolate or relative z-index to action-bar wrapper if needed\n- Remove the two // FIX: comments in internal/web/components/action_bar.templ\n- Regenerate templ code\n- Rebuild CSS with Tailwind\n\nKey files:\n- static/css/app.css:309-311 — .action-menu definition\n- internal/web/components/action_bar.templ:92-93 — FIX comments\n- static/css/app.css:410-411 — .feed-card animation\n\nVerify on homepage feed and profile brews tab.","status":"open","priority":"normal","created_at":"2026-02-08T22:48:06.956664383Z","updated_at":"2026-02-08T22:48:06.956664383Z"}
+4 -3
internal/web/components/action_bar.templ
··· 29 // ActionBar renders the action bar with Comments, Like, Share, and More menu 30 // Order: [💬 Comments] [♡ Like] [↗ Share] [⋯ More] 31 templ ActionBar(props ActionBarProps) { 32 - <div class="action-bar" x-data="{ moreOpen: false }"> 33 <!-- Comments (placeholder) --> 34 <button 35 type="button" ··· 57 @ActionBarShareButton(props) 58 } 59 <!-- More menu --> 60 - <div class="relative"> 61 <button 62 type="button" 63 - @click="moreOpen = !moreOpen" 64 @click.away="moreOpen = false" 65 class="action-btn" 66 aria-label="More options" ··· 79 x-transition:leave-start="transform opacity-100 scale-100" 80 x-transition:leave-end="transform opacity-0 scale-95" 81 class="action-menu" 82 x-cloak 83 > 84 if props.IsOwner {
··· 29 // ActionBar renders the action bar with Comments, Like, Share, and More menu 30 // Order: [💬 Comments] [♡ Like] [↗ Share] [⋯ More] 31 templ ActionBar(props ActionBarProps) { 32 + <div class="action-bar" x-data="{ moreOpen: false, openUp: true }"> 33 <!-- Comments (placeholder) --> 34 <button 35 type="button" ··· 57 @ActionBarShareButton(props) 58 } 59 <!-- More menu --> 60 + <div class="relative z-10"> 61 <button 62 type="button" 63 + @click="if (!moreOpen) { openUp = $el.getBoundingClientRect().top > window.innerHeight * 0.25 }; moreOpen = !moreOpen" 64 @click.away="moreOpen = false" 65 class="action-btn" 66 aria-label="More options" ··· 79 x-transition:leave-start="transform opacity-100 scale-100" 80 x-transition:leave-end="transform opacity-0 scale-95" 81 class="action-menu" 82 + :class="openUp ? 'bottom-full mb-1' : 'top-full mt-1'" 83 x-cloak 84 > 85 if props.IsOwner {
+1 -1
internal/web/components/layout.templ
··· 73 <link rel="icon" href="/static/favicon.svg" type="image/svg+xml"/> 74 <link rel="icon" href="/static/favicon-32.svg" type="image/svg+xml" sizes="32x32"/> 75 <link rel="apple-touch-icon" href="/static/icon-192.svg"/> 76 - <link rel="stylesheet" href="/static/css/output.css?v=0.5.0"/> 77 <style> 78 [x-cloak] { display: none !important; } 79 </style>
··· 73 <link rel="icon" href="/static/favicon.svg" type="image/svg+xml"/> 74 <link rel="icon" href="/static/favicon-32.svg" type="image/svg+xml" sizes="32x32"/> 75 <link rel="apple-touch-icon" href="/static/icon-192.svg"/> 76 + <link rel="stylesheet" href="/static/css/output.css?v=0.5.1"/> 77 <style> 78 [x-cloak] { display: none !important; } 79 </style>
+1 -1
static/css/app.css
··· 307 308 /* Action Menu (More dropdown) */ 309 .action-menu { 310 - @apply absolute right-0 bottom-full mb-1 w-36 bg-white rounded-lg shadow-lg border border-brown-200 py-1 z-10; 311 } 312 313 /* Dropdown menu (top-positioned, for headers/nav) */
··· 307 308 /* Action Menu (More dropdown) */ 309 .action-menu { 310 + @apply absolute left-1/2 -translate-x-1/2 w-36 bg-white rounded-lg shadow-lg border border-brown-200 py-1 z-50; 311 } 312 313 /* Dropdown menu (top-positioned, for headers/nav) */