feat: Privacy settings with functional toggles, sticky sidebar, roadmap doc
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run

This commit is contained in:
fullsizemalt 2025-12-23 13:08:48 -08:00
parent 824a70d303
commit 2da46eaa16
5 changed files with 201 additions and 66 deletions

View file

@ -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")

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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 (
<div className="flex items-center justify-center min-h-[50vh]">
@ -167,8 +200,8 @@ export default function SettingsPage() {
{/* Desktop: Side-by-side layout, Mobile: Tabs */}
<div className="hidden lg:grid lg:grid-cols-[280px_1fr] lg:gap-8">
{/* Sidebar Navigation */}
<nav className="space-y-1">
{/* Sidebar Navigation - Sticky */}
<nav className="space-y-1 sticky top-24 self-start">
<SidebarLink icon={User} label="Profile" href="#profile" active />
<SidebarLink icon={Palette} label="Appearance" href="#appearance" />
<SidebarLink icon={Eye} label="Display" href="#display" />
@ -215,7 +248,7 @@ export default function SettingsPage() {
<Separator />
<PrivacySection />
<PrivacySection settings={privacySettings} onChange={handlePrivacyChange} />
</div>
</div>
@ -278,7 +311,7 @@ export default function SettingsPage() {
</TabsContent>
<TabsContent value="privacy">
<PrivacySection />
<PrivacySection settings={privacySettings} onChange={handlePrivacyChange} />
</TabsContent>
</Tabs>
</div>
@ -568,7 +601,10 @@ function NotificationsSection() {
}
// Privacy Section
function PrivacySection() {
function PrivacySection({ settings, onChange }: {
settings: { profile_public: boolean; show_attendance_public: boolean; appear_in_leaderboards: boolean };
onChange: (key: string, value: boolean) => void;
}) {
return (
<Card id="privacy">
<CardHeader>
@ -584,9 +620,8 @@ function PrivacySection() {
<SettingRow
label="Public Profile"
description="Allow other users to view your profile, attendance history, and reviews"
checked={true}
onChange={() => { }}
comingSoon
checked={settings.profile_public}
onChange={(checked) => onChange("profile_public", checked)}
/>
<Separator />
@ -594,9 +629,8 @@ function PrivacySection() {
<SettingRow
label="Show Attendance"
description="Display which shows you've attended on your public profile"
checked={true}
onChange={() => { }}
comingSoon
checked={settings.show_attendance_public}
onChange={(checked) => onChange("show_attendance_public", checked)}
/>
<Separator />
@ -604,9 +638,8 @@ function PrivacySection() {
<SettingRow
label="Appear in Leaderboards"
description="Allow your name to appear on community leaderboards"
checked={true}
onChange={() => { }}
comingSoon
checked={settings.appear_in_leaderboards}
onChange={(checked) => onChange("appear_in_leaderboards", checked)}
/>
<Separator />