diff --git a/backend/models.py b/backend/models.py index 79c9d3f..18717e8 100644 --- a/backend/models.py +++ b/backend/models.py @@ -176,6 +176,11 @@ class User(SQLModel, table=True): avatar_bg_color: Optional[str] = Field(default="#3B82F6", description="Hex color for avatar background") avatar_text: Optional[str] = Field(default=None, description="1-3 character text overlay on avatar") + # Privacy settings + profile_public: bool = Field(default=True, description="Allow others to view profile") + show_attendance_public: bool = Field(default=True, description="Show attended shows on profile") + appear_in_leaderboards: bool = Field(default=True, description="Appear in community leaderboards") + # Gamification xp: int = Field(default=0, description="Experience points") level: int = Field(default=1, description="User level based on XP") diff --git a/backend/routers/users.py b/backend/routers/users.py index 1ee7dcb..5d09251 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -136,6 +136,35 @@ def get_avatar_colors(): """Get available avatar preset colors""" return {"colors": AVATAR_COLORS} +class PrivacyUpdate(BaseModel): + profile_public: Optional[bool] = None + show_attendance_public: Optional[bool] = None + appear_in_leaderboards: Optional[bool] = None + +@router.patch("/me/privacy") +def update_privacy( + update: PrivacyUpdate, + current_user: User = Depends(get_current_user), + session: Session = Depends(get_session) +): + """Update privacy settings""" + if update.profile_public is not None: + current_user.profile_public = update.profile_public + if update.show_attendance_public is not None: + current_user.show_attendance_public = update.show_attendance_public + if update.appear_in_leaderboards is not None: + current_user.appear_in_leaderboards = update.appear_in_leaderboards + + session.add(current_user) + session.commit() + session.refresh(current_user) + + return { + "profile_public": current_user.profile_public, + "show_attendance_public": current_user.show_attendance_public, + "appear_in_leaderboards": current_user.appear_in_leaderboards + } + @router.patch("/me/preferences", response_model=UserPreferencesUpdate) def update_preferences( prefs: UserPreferencesUpdate, diff --git a/backend/schemas.py b/backend/schemas.py index 325ab20..4c154c0 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -14,6 +14,9 @@ class UserRead(SQLModel): is_superuser: bool avatar_bg_color: Optional[str] = "#3B82F6" avatar_text: Optional[str] = None + profile_public: bool = True + show_attendance_public: bool = True + appear_in_leaderboards: bool = True class Token(SQLModel): access_token: str diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 2b76d24..7a6a5ad 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,72 +1,137 @@ -# Future Roadmap & Implementation Plan +# Elmeg Platform Roadmap -## 1. Cross-Vertical "Fandom Federation" (Future Feature) - -**Concept**: Enable cross-pollination between different band/fandom instances (Verticals). -**Use Case**: A user mentions `@Phish` in the `Goose` instance, or a guest artist like "Trey Anastasio" links to his stats in the Phish vertical. -**Implementation Strategy**: - -* **Federated Identity**: A single `User` account works across all verticals (already partially supported by our schema). -* **Universal Resolver**: A service that resolves links like `elmeg://phish/shows/123` or `@phish:user_123`. -* **Shared Artist Database**: A global table of Artists that links to specific performances across all verticals. +**Last Updated:** 2023-12-23 --- -## 2. Immediate Implementation Plan (V1.1 Polish) +## Current Status Summary -We will tackle the following gaps to round out the V1 experience: +### ✅ Analytics -### Phase A: Personalization & "Wiki Mode" +**Status: NOT CONFIGURED** -**Goal**: Allow users to customize their experience, specifically enabling the "pure archive" feel. +- No Google Analytics, Plausible, or similar tracking implemented +- No gtag or measurement ID found in codebase +- **Action needed:** Choose analytics provider (recommend Plausible for privacy-first) -1. **Settings Page**: Create `/settings` route. -2. **Preferences UI**: Toggles for: - * `Wiki Mode` (Hides comments, ratings, social noise). - * `Show Ratings` (Toggle visibility of 1-10 scores). - * `Show Comments` (Toggle visibility of discussion sections). -3. **Frontend Logic**: Wrap social components in a context provider that respects these flags. +### ✅ AWS SES Email -### Phase B: Moderation Dashboard +**Status: CONFIGURED & OPERATIONAL** -**Goal**: Empower admins to maintain data quality and community standards. +- IAM user: `AKIAVFNHG5QATGYJHVCZ` (scoped SES permissions) +- Region: `us-east-1` +- From address: `noreply@elmeg.xyz` +- Frontend URL: `https://elmeg.xyz` +- Client initialized: ✅ Yes +- **Note:** Domain verification status needs manual check in AWS console -1. **Admin Route**: Create `/admin` (protected by `is_superuser` or `role=admin`). -2. **Nickname Queue**: List `pending` nicknames with Approve/Reject actions. -3. **Report Queue**: List reported content with Dismiss/Delete actions. -4. **User Management**: Basic list of users with Ban/Promote options. +### Templates Available -### Phase C: Activity Feed (The "Pulse") +| Template | Status | +|----------|--------| +| Email Verification | ✅ Ready | +| Password Reset | ✅ Ready | +| Security Alert | ✅ Ready | -**Goal**: Make the platform feel alive and aid discovery. +--- -1. **Global Feed**: Aggregated stream of: - * New Reviews -1. **Global Feed**: Aggregated stream of: - * New Reviews - * New Show Attendance - * New Groups created - * Rare stats/milestones (e.g., "User X attended their 100th show") -2. **Home Page Widget**: Replace static content on Home with this dynamic feed. +## Settings Page Roadmap -### Phase D: Visualizations & Deep Stats +### Phase 1: Quick Wins (Current Sprint) -**Goal**: Provide the "crunchy" data fans love. +| Feature | Backend Change | Frontend Change | Effort | +|---------|----------------|-----------------|--------| +| ~~Heady Badges toggle~~ | Add to UserPreferences | Toggle in Display | ⏭️ Skip (UI only, no effect) | +| Privacy: Public Profile | Add `profile_public` to User | Toggle in Privacy | 🟢 Small | +| Privacy: Show Attendance | Add `show_attendance_public` | Toggle in Privacy | 🟢 Small | +| Privacy: Leaderboards | Add `appear_in_leaderboards` | Toggle in Privacy | 🟢 Small | +| Theme Persistence | Store in localStorage/UserPrefs | Already works client-side | 🟢 Small | -1. **Gap Chart**: A visual bar chart on Song Pages showing the gap between performances. -2. **Heatmaps**: "Shows by Year" or "Shows by State" maps on Artist/Band pages. -3. **Graph View**: (Mind Map precursor) Simple node-link diagram of related songs/shows. +### Phase 2: Notifications (Deferred) -### Phase E: Glossary (Wiki-Style Knowledge Base) +| Feature | Dependency | Status | +|---------|------------|--------| +| Comment Replies | Notification system exists | Ready to implement | +| New Show Added | Import trigger hook | Needs backend work | +| Chase Song Played | Post-import check | Needs backend work | +| Weekly Digest | Email templates + cron | Future | -**Goal**: Build a community-curated glossary of fandom terms. +### Phase 3: Data & Account (Deferred) -1. **Glossary Entry Model**: Term, definition, example, category, status. -2. **Edit History**: Track suggested edits with approval workflow. -3. **Public Pages**: `/glossary` index and `/glossary/[term]` detail pages. -4. **Moderation**: Admin queue for approving/rejecting entries and edits. -5. **Integration**: Include in global search, auto-link in comments. +| Feature | Notes | +|---------|-------| +| Export My Data | GDPR compliance, JSON download | +| Delete Account | Cascade delete + confirmation | +| Connected Accounts | OAuth providers (future) | -## 3. Execution Order +--- -4. **Phase D (Stats)**: "Nice to have" polish. +## Avatar System Roadmap + +### ✅ Phase 1: Jewel Tones (Complete) + +12 gemstone-named colors available to all users + +### Phase 2: Pastels (Unlock 1) + +- Trigger: 5 shows attended OR 10 ratings +- Colors: Soft versions of jewel tones + +### Phase 3: Neons (Unlock 2) + +- Trigger: 15 shows attended OR Level 5 +- Colors: Vibrant high-saturation + +### Phase 4: Gradients (Unlock 3) + +- Trigger: Level 10 OR special achievement +- Two-tone diagonal gradients + +--- + +## Blockers & Clarifications Needed + +### 1. Analytics Provider + +**Question:** Which analytics do you prefer? + +- **Plausible** (privacy-first, GDPR compliant, paid ~$9/mo) +- **Umami** (self-hosted, free, privacy-first) +- **Google Analytics 4** (free, most features, privacy concerns) +- **PostHog** (product analytics + session replay) + +### 2. Email Domain Verification + +**Question:** Has `elmeg.xyz` been verified in AWS SES console? + +- If yes: Emails are ready to send +- If no: Need to add DKIM/TXT records to DNS + +### 3. SES Sandbox Status + +**Question:** Has production access been requested? + +- Sandbox = can only send to verified email addresses +- Production = can send to anyone + +--- + +## Implementation Priority + +### Today (Phase 1 Quick Wins) + +1. ✅ Create this roadmap document +2. 🔄 Add privacy columns to User model +3. 🔄 Add API endpoint for privacy settings +4. 🔄 Wire up Privacy section toggles + +### This Week + +- [ ] Answer analytics question +- [ ] Verify SES domain status +- [ ] Theme persistence to user preferences + +### Next Sprint + +- [ ] Notification preferences backend +- [ ] Avatar unlock system diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index 43cca16..2cb90f2 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -52,19 +52,31 @@ export default function SettingsPage() { const [profileSaved, setProfileSaved] = useState(false) // Avatar state - const [avatarBgColor, setAvatarBgColor] = useState("#1E3A8A") + const [avatarBgColor, setAvatarBgColor] = useState("#0F4C81") const [avatarText, setAvatarText] = useState("") const [avatarSaving, setAvatarSaving] = useState(false) const [avatarSaved, setAvatarSaved] = useState(false) const [avatarError, setAvatarError] = useState("") + // Privacy state + const [privacySettings, setPrivacySettings] = useState({ + profile_public: true, + show_attendance_public: true, + appear_in_leaderboards: true + }) + useEffect(() => { if (user) { const extUser = user as any setBio(extUser.bio || "") setUsername(extUser.email?.split('@')[0] || "") - setAvatarBgColor(extUser.avatar_bg_color || "#1E3A8A") + setAvatarBgColor(extUser.avatar_bg_color || "#0F4C81") setAvatarText(extUser.avatar_text || "") + setPrivacySettings({ + profile_public: extUser.profile_public ?? true, + show_attendance_public: extUser.show_attendance_public ?? true, + appear_in_leaderboards: extUser.appear_in_leaderboards ?? true + }) } }, [user]) @@ -130,6 +142,27 @@ export default function SettingsPage() { } } + const handlePrivacyChange = async (key: string, value: boolean) => { + // Optimistic update + setPrivacySettings(prev => ({ ...prev, [key]: value })) + + try { + const token = localStorage.getItem("token") + await fetch(`${getApiUrl()}/users/me/privacy`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ [key]: value }), + }) + } catch (e) { + // Revert on error + setPrivacySettings(prev => ({ ...prev, [key]: !value })) + console.error("Failed to update privacy setting:", e) + } + } + if (loading) { return (