BREATHE / FUTURE_ENHANCEMENTS.md
tannuiscoding's picture
added app.py
5a264f5
# 🚀 BREATHE — Future Enhancements
A living document of planned and possible improvements to the BREATHE platform.
---
## 1. PostgreSQL — Persistent Production Database
SQLite is fine for development but it resets or corrupts when the server restarts on many cloud platforms. PostgreSQL is a proper production-grade database where your data persists independently of the server.
### Why switch?
- Data survives server restarts, crashes, and re-deployments
- Multiple workers can read/write simultaneously (SQLite locks)
- You can inspect, back up, and query data directly from any Postgres client
- Required by most cloud platforms (Railway, Render, Supabase, AWS RDS, etc.)
### Step-by-step setup (local)
**1. Install PostgreSQL**
```bash
# macOS (Homebrew)
brew install postgresql@16
brew services start postgresql@16
# Ubuntu / Debian
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql
```
**2. Create the database and user**
```bash
psql postgres
```
```sql
CREATE USER breathe_user WITH PASSWORD 'your_strong_password';
CREATE DATABASE breathe_db OWNER breathe_user;
GRANT ALL PRIVILEGES ON DATABASE breathe_db TO breathe_user;
\q
```
**3. Install the Python driver**
```bash
source venv/bin/activate
pip install psycopg2-binary
```
**4. Update `.env`**
```
DATABASE_URL=postgresql://breathe_user:your_strong_password@localhost:5432/breathe_db
```
**5. Run the app — tables are created automatically**
```bash
python app.py
```
Flask's `db.create_all()` will create all tables in Postgres on first run.
**6. Verify**
```bash
psql -U breathe_user -d breathe_db -c "\dt"
# Should list: users, assessments, gratitude_entries
```
### Step-by-step setup (cloud — Supabase, free tier)
1. Go to [supabase.com](https://supabase.com) → New project
2. In **Settings → Database** copy the **Connection string** (URI format)
3. Paste it into `.env` as `DATABASE_URL`
4. The app connects and creates tables on next start — no other changes needed
### Step-by-step setup (cloud — Railway)
1. Go to [railway.app](https://railway.app) → New project → Add PostgreSQL
2. Click the Postgres service → **Connect** tab → copy the `DATABASE_URL`
3. Add it as an environment variable in your BREATHE service on Railway
4. Re-deploy — done
### Keeping data safe with backups
```bash
# Dump the entire database
pg_dump -U breathe_user breathe_db > backup_$(date +%Y%m%d).sql
# Restore from a dump
psql -U breathe_user breathe_db < backup_20260502.sql
```
---
## 2. Sign Up / Log In with Google (OAuth 2.0)
Let users authenticate with their Google account — no password to remember.
### How it works
1. User clicks "Continue with Google"
2. Browser redirects to Google's OAuth consent screen
3. Google returns an authorization code to your callback URL
4. Flask exchanges the code for an access token and reads the user's profile (name, email, avatar)
5. Your app creates or logs in the user automatically
### Backend — Flask-Dance (simplest approach)
**Install**
```bash
pip install flask-dance[sqla]
```
**Register a Google OAuth app**
1. Go to [console.cloud.google.com](https://console.cloud.google.com)
2. Create a new project → **APIs & Services → Credentials**
3. Click **Create Credentials → OAuth client ID**
4. Application type: **Web application**
5. Authorised redirect URI: `http://localhost:5000/login/google/authorized`
6. Copy the **Client ID** and **Client Secret** into `.env`:
```
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
```
**Add to `backend/__init__.py`**
```python
from flask_dance.contrib.google import make_google_blueprint, google
google_bp = make_google_blueprint(
client_id=os.environ.get("GOOGLE_CLIENT_ID"),
client_secret=os.environ.get("GOOGLE_CLIENT_SECRET"),
scope=["openid", "email", "profile"],
redirect_url="/api/auth/google/callback",
)
app.register_blueprint(google_bp, url_prefix="/login")
```
**Add a callback route in `auth.py`**
```python
from flask_dance.contrib.google import google
@auth_bp.route("/google/callback")
def google_callback():
if not google.authorized:
return jsonify({"error": "Not authorized"}), 401
resp = google.get("/oauth2/v2/userinfo")
info = resp.json()
user = User.query.filter_by(email=info["email"]).first()
if not user:
user = User(username=info["name"], email=info["email"])
user.avatar = info.get("picture")
db.session.add(user)
db.session.commit()
session["user_id"] = user.id
return redirect("http://localhost:5173/app/breathe")
```
**Frontend — add a Google button to `AuthPage.jsx`**
```jsx
<a href="http://localhost:5000/login/google" className="btn-google">
<img src="/google-icon.svg" alt="" /> Continue with Google
</a>
```
---
## 3. Improved UI & UX
### Ideas to implement
| Area | Enhancement |
|------|-------------|
| Onboarding | First-time walkthrough tooltip tour (using `driver.js` or `intro.js`) |
| Themes | Light mode toggle + system preference detection |
| Animations | Framer Motion page transitions and micro-interactions |
| Mobile | Full PWA support — installable on phone home screen |
| Accessibility | ARIA labels, keyboard navigation, high-contrast mode |
| Charts | Hover annotations, zoom on timeline, weekly/monthly toggle |
| Notifications | Browser push notifications for daily assessment reminders |
| Streaks | Gamification — show journaling streak counter on dashboard |
| Data export | Download your assessment + journal history as CSV or PDF |
### Quick win — dark/light theme toggle
```js
// in index.css: add a [data-theme="light"] block overriding --bg, --surface, etc.
// in a ThemeToggle component:
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light')
localStorage.setItem('theme', isDark ? 'dark' : 'light')
```
---
## 4. Calm Background Music
Play ambient/calming audio tracks inside the app to accompany breathing and journaling sessions.
### Option A — Self-hosted audio files (simplest)
1. Download royalty-free tracks from [freemusicarchive.org](https://freemusicarchive.org) or [pixabay.com/music](https://pixabay.com/music/)
2. Place `.mp3` files in `frontend/public/audio/`
3. Build a `MusicPlayer` component:
```jsx
// frontend/src/components/MusicPlayer.jsx
import { useState, useRef } from 'react'
const TRACKS = [
{ title: 'Forest Rain', src: '/audio/forest-rain.mp3' },
{ title: 'Ocean Waves', src: '/audio/ocean-waves.mp3' },
{ title: 'Tibetan Bowls', src: '/audio/tibetan-bowls.mp3' },
]
export default function MusicPlayer() {
const [playing, setPlaying] = useState(false)
const [track, setTrack] = useState(0)
const audioRef = useRef(null)
function togglePlay() {
if (playing) { audioRef.current.pause() }
else { audioRef.current.play() }
setPlaying(!playing)
}
function changeTrack(i) {
setTrack(i)
setPlaying(false)
setTimeout(() => { audioRef.current.load(); audioRef.current.play(); setPlaying(true) }, 50)
}
return (
<div className="music-player">
<audio ref={audioRef} loop src={TRACKS[track].src} />
<button onClick={togglePlay}>{playing ? '⏸' : '▶'}</button>
<span>{TRACKS[track].title}</span>
{TRACKS.map((t, i) => (
<button key={i} onClick={() => changeTrack(i)}>{t.title}</button>
))}
</div>
)
}
```
4. Add `<MusicPlayer />` to `BreathePage.jsx` or the guided exercise modal
### Option B — Streaming via YouTube IFrame API (no file hosting needed)
```jsx
// Embed a YouTube ambient playlist
<iframe
src="https://www.youtube.com/embed/videoseries?list=PLQ6T_LmSTMi17X70BIqNfSFRMC1u83M7m&autoplay=1&loop=1"
allow="autoplay"
style={{ display: 'none' }} // audio only
/>
```
---
## 5. Text-to-Speech for Activity Instructions
Convert the step-by-step instructions in guided exercises into spoken audio so users can close their eyes and follow along hands-free.
### Option A — Web Speech API (free, no API key, built into browser)
```jsx
// frontend/src/utils/tts.js
export function speak(text, { rate = 0.85, pitch = 1, volume = 1 } = {}) {
if (!window.speechSynthesis) return
window.speechSynthesis.cancel()
const utt = new SpeechSynthesisUtterance(text)
utt.rate = rate
utt.pitch = pitch
utt.volume = volume
// Pick a calm voice if available
const voices = window.speechSynthesis.getVoices()
const calm = voices.find(v => v.name.includes('Samantha') || v.name.includes('Karen'))
if (calm) utt.voice = calm
window.speechSynthesis.speak(utt)
}
export function stopSpeaking() {
window.speechSynthesis.cancel()
}
```
**Use it in `BreathePage.jsx` guided modal:**
```jsx
import { speak, stopSpeaking } from '../utils/tts'
// When step changes, read it aloud:
useEffect(() => {
speak(`${s.label}. ${s.desc}`)
return () => stopSpeaking()
}, [step])
// Add a toggle button:
const [ttsOn, setTtsOn] = useState(false)
```
**Add a count-down timer with spoken cues for Box Breathing:**
```jsx
useEffect(() => {
if (!s.duration || !ttsOn) return
speak(`${s.label}. ${s.duration} seconds.`)
const timer = setTimeout(() => speak('Next.'), s.duration * 1000)
return () => clearTimeout(timer)
}, [step])
```
### Option B — ElevenLabs API (natural-sounding AI voices)
1. Sign up at [elevenlabs.io](https://elevenlabs.io) (free tier: 10,000 chars/month)
2. Generate MP3 files for each step offline and include them as static assets (same as Option A of music)
3. Or call the API at runtime:
```python
# backend/routes/tts.py
import requests, os
from flask import Blueprint, jsonify, request
tts_bp = Blueprint("tts", __name__, url_prefix="/api/tts")
@tts_bp.route("/speak", methods=["POST"])
def speak():
text = (request.get_json() or {}).get("text", "")[:500]
resp = requests.post(
"https://api.elevenlabs.io/v1/text-to-speech/EXAVITQu4vr4xnSDxMaL",
headers={"xi-api-key": os.environ["ELEVEN_API_KEY"]},
json={"text": text, "voice_settings": {"stability": 0.5, "similarity_boost": 0.75}},
)
return resp.content, 200, {"Content-Type": "audio/mpeg"}
```
```js
// Frontend: fetch and play
const res = await fetch('/api/tts/speak', { method:'POST', body: JSON.stringify({text}), headers:{'Content-Type':'application/json'} })
const blob = await res.blob()
new Audio(URL.createObjectURL(blob)).play()
```
### Option C — Google Cloud Text-to-Speech (highest quality, WaveNet voices)
```bash
pip install google-cloud-texttospeech
```
```python
from google.cloud import texttospeech
client = texttospeech.TextToSpeechClient()
synthesis_input = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(
language_code="en-US",
name="en-US-Wavenet-F",
)
audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3)
response = client.synthesize_speech(input=synthesis_input, voice=voice, audio_config=audio_config)
# response.audio_content is bytes → stream to frontend
```
---
## 6. Spotify Integration — Play Your Playlists Inside BREATHE
Let users connect their Spotify account and play their own playlists (or curated calm playlists) without leaving the app.
### How it works (Spotify Web Playback SDK + OAuth PKCE)
**Step 1 — Create a Spotify app**
1. Go to [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard)
2. Click **Create App**
3. Set **Redirect URI** to `http://localhost:5173/app/spotify/callback`
4. Copy your **Client ID** (no secret needed for PKCE flow)
5. Add to `.env`: `VITE_SPOTIFY_CLIENT_ID=your_client_id`
**Step 2 — Authorization (PKCE, no backend needed)**
```js
// frontend/src/utils/spotify.js
const CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID
const REDIRECT_URI = 'http://localhost:5173/app/spotify/callback'
const SCOPES = 'streaming user-read-email user-read-private user-library-read playlist-read-private'
export async function loginWithSpotify() {
const verifier = generateCodeVerifier(128)
const challenge = await generateCodeChallenge(verifier)
localStorage.setItem('spotify_verifier', verifier)
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
scope: SCOPES,
redirect_uri: REDIRECT_URI,
code_challenge_method: 'S256',
code_challenge: challenge,
})
window.location = 'https://accounts.spotify.com/authorize?' + params
}
export async function exchangeToken(code) {
const verifier = localStorage.getItem('spotify_verifier')
const res = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: verifier,
}),
})
const data = await res.json()
localStorage.setItem('spotify_token', data.access_token)
localStorage.setItem('spotify_refresh', data.refresh_token)
return data.access_token
}
// Helper: generate PKCE verifier/challenge
function generateCodeVerifier(length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
return Array.from(crypto.getRandomValues(new Uint8Array(length)))
.map(b => chars[b % chars.length]).join('')
}
async function generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier)
const digest = await crypto.subtle.digest('SHA-256', data)
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
}
```
**Step 3 — Load the Spotify Web Playback SDK**
```html
<!-- frontend/index.html -->
<script src="https://sdk.scdn.co/spotify-player.js"></script>
```
**Step 4 — Create a `SpotifyPlayer` component**
```jsx
// frontend/src/components/SpotifyPlayer.jsx
import { useEffect, useState } from 'react'
export default function SpotifyPlayer({ token }) {
const [player, setPlayer] = useState(null)
const [deviceId, setDeviceId] = useState(null)
const [playing, setPlaying] = useState(false)
const [track, setTrack] = useState(null)
const [playlists, setPlaylists] = useState([])
useEffect(() => {
window.onSpotifyWebPlaybackSDKReady = () => {
const p = new window.Spotify.Player({
name: 'BREATHE Player',
getOAuthToken: cb => cb(token),
volume: 0.5,
})
p.addListener('ready', ({ device_id }) => setDeviceId(device_id))
p.addListener('player_state_changed', state => {
if (!state) return
setTrack(state.track_window.current_track)
setPlaying(!state.paused)
})
p.connect()
setPlayer(p)
}
}, [token])
// Fetch user's playlists
useEffect(() => {
fetch('https://api.spotify.com/v1/me/playlists', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json()).then(d => setPlaylists(d.items || []))
}, [token])
async function playPlaylist(uri) {
await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${deviceId}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ context_uri: uri }),
})
}
return (
<div className="spotify-player">
{track && (
<div className="sp-now-playing">
<img src={track.album.images[0]?.url} alt="" className="sp-album-art" />
<div>
<div className="sp-track-name">{track.name}</div>
<div className="sp-artist">{track.artists.map(a => a.name).join(', ')}</div>
</div>
<button onClick={() => player.togglePlay()}>{playing ? '⏸' : '▶'}</button>
<button onClick={() => player.previousTrack()}>⏮</button>
<button onClick={() => player.nextTrack()}>⏭</button>
</div>
)}
<div className="sp-playlists">
{playlists.map(pl => (
<button key={pl.id} onClick={() => playPlaylist(pl.uri)}>
{pl.images[0] && <img src={pl.images[0].url} alt="" />}
{pl.name}
</button>
))}
</div>
</div>
)
}
```
**Step 5 — Add a callback route in React**
```jsx
// App.jsx — add inside <Routes>
<Route path="/app/spotify/callback" element={<SpotifyCallback />} />
```
```jsx
// pages/SpotifyCallback.jsx
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { exchangeToken } from '../utils/spotify'
export default function SpotifyCallback() {
const navigate = useNavigate()
useEffect(() => {
const code = new URLSearchParams(window.location.search).get('code')
if (code) exchangeToken(code).then(() => navigate('/app/breathe'))
}, [])
return <div>Connecting to Spotify…</div>
}
```
**Step 6 — Add "Connect Spotify" button to the Breathe hub or Dashboard**
```jsx
import { loginWithSpotify } from '../utils/spotify'
const token = localStorage.getItem('spotify_token')
if (!token) {
return <button onClick={loginWithSpotify}>🎵 Connect Spotify</button>
}
return <SpotifyPlayer token={token} />
```
> **Note:** The Spotify Web Playback SDK requires a **Spotify Premium** account to play audio.
> Free accounts can still fetch playlist metadata but cannot stream tracks directly.
---
## 7. Other Future Enhancements
| Enhancement | Notes |
|-------------|-------|
| **Wearable data sync** | Import heart rate & sleep data from Apple Health / Google Fit via their REST APIs and use it as auto-filled psychometric inputs |
| **AI chat support** | Add a "Talk to BREATHE" widget using OpenAI's chat API — gives personalised coping suggestions based on the user's stress history |
| **Mood tracker** | Daily one-tap mood log (separate from assessments) charted over time |
| **Weekly report email** | Cron job + Flask-Mail sends a weekly PDF summary of stress trends |
| **Multi-language support** | i18n with `react-i18next` — translate UI strings and TTS language |
| **Community / anonymous sharing** | Opt-in feed where users share their coping strategies anonymously |
| **Therapist portal** | Separate role for mental-health professionals to view consented patient dashboards |
| **Mobile app** | Wrap the React frontend with Capacitor or React Native for iOS/Android |
| **Offline mode** | Service Worker caches the app shell; assessments queue and sync when back online |
| **Biometric login** | WebAuthn passkey support — log in with Face ID or fingerprint |