Skip to content

Commit 8531371

Browse files
authored
feat: add showcase mode to disable all AI/Gemini features (#105)
- Add SHOWCASE_MODE env variable (defaults to true) - Disable all Celery scheduled tasks when in showcase mode - Block all API endpoints that trigger AI tasks: - /api/admin/tasks/* (collect, generate-alerts, etc.) - /api/bluesky/trigger - /api/archive/trigger - /api/notifications/email/batch - Block geocoding endpoints: - /auth/geocode - /api/alerts/regions/search - Add showcase mode checks to services: - analysis.py (Gemini AI) - geocoding_service.py (Google Geocoding) - population_estimator.py (Google reverse geocoding) - Update docker-compose.yml with SHOWCASE_MODE for all services - Add showcase mode banner to admin dev-tools page - Graceful error handling in location onboarding This puts the app in portfolio/demo mode with $0 API costs while keeping all read-only functionality working.
1 parent 92a399f commit 8531371

File tree

16 files changed

+189
-54
lines changed

16 files changed

+189
-54
lines changed

client/app/admin/dev-tools/page.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ export default function DevToolsPage() {
152152
)}
153153
</div>
154154

155+
{/* Showcase Mode Banner */}
156+
<Card className="border-amber-500/50 bg-amber-500/10">
157+
<CardContent className="pt-6">
158+
<div className="flex items-center gap-3">
159+
<span className="text-2xl">🎭</span>
160+
<div>
161+
<h3 className="font-semibold text-amber-600 dark:text-amber-400">Showcase Mode Active</h3>
162+
<p className="text-sm text-muted-foreground">
163+
AI features and data collection are disabled to prevent API costs.
164+
The app displays existing data only for portfolio demonstration.
165+
</p>
166+
</div>
167+
</div>
168+
</CardContent>
169+
</Card>
170+
155171
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
156172
<Card>
157173
<CardHeader>

client/components/location-onboarding.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,10 @@ export function LocationOnboarding({ onLocationSet }: LocationOnboardingProps) {
149149
const response = await apiClient(`/auth/geocode?query=${encodeURIComponent(manualLocation.trim())}`);
150150

151151
if (!response.ok) {
152-
if (response.status === 404) {
152+
if (response.status === 403) {
153+
// Showcase mode - geocoding disabled
154+
setError('Manual location search is currently unavailable. Please use "Auto-Detect Country" or "Skip Location Setup" instead.');
155+
} else if (response.status === 404) {
153156
setError('Location not found. Please try a different location.');
154157
} else {
155158
setError('Failed to find location. Please try again.');

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ services:
5050
DATABASE_URL: postgresql://dev:devpassword@postgres:5432/bluerelief
5151
REDIS_URL: redis://redis:6379/0
5252
ENVIRONMENT: ${ENVIRONMENT}
53+
SHOWCASE_MODE: ${SHOWCASE_MODE:-true}
5354
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
5455
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
5556
JWT_SECRET_KEY: ${JWT_SECRET_KEY}
@@ -90,6 +91,7 @@ services:
9091
environment:
9192
DATABASE_URL: postgresql://dev:devpassword@postgres:5432/bluerelief
9293
REDIS_URL: redis://redis:6379/0
94+
SHOWCASE_MODE: ${SHOWCASE_MODE:-true}
9395
BlueSky_Username: ${BlueSky_Username}
9496
BlueSky_Password: ${BlueSky_Password}
9597
GOOGLE_API_KEY: ${GOOGLE_API_KEY}
@@ -120,6 +122,7 @@ services:
120122
environment:
121123
DATABASE_URL: postgresql://dev:devpassword@postgres:5432/bluerelief
122124
REDIS_URL: redis://redis:6379/0
125+
SHOWCASE_MODE: ${SHOWCASE_MODE:-true}
123126
BlueSky_Username: ${BlueSky_Username}
124127
BlueSky_Password: ${BlueSky_Password}
125128
GOOGLE_API_KEY: ${GOOGLE_API_KEY}

email-service/src/templates/logo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ export const BASE_URL = 'https://bluerelief.app';
77

88

99

10+
11+

environment.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ REDIS_URL=redis://localhost:6379/0
1212
ENVIRONMENT=development
1313
NODE_ENV=development
1414

15+
# Showcase Mode - When true, disables all AI/Gemini features to prevent API costs
16+
# Set to false to enable data collection and AI analysis
17+
SHOWCASE_MODE=true
18+
1519
# Frontend Configuration
1620
NEXT_PUBLIC_API_URL=http://localhost:8000
1721
NEXT_PUBLIC_APP_URL=https://platform.private.bluerelief.app

server/celery_app.py

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
99

10+
# SHOWCASE_MODE: When enabled, disables all automatic tasks to prevent API costs
11+
SHOWCASE_MODE = os.getenv("SHOWCASE_MODE", "true").lower() == "true"
12+
1013
celery_app = Celery(
1114
"bluerelief_tasks",
1215
broker=REDIS_URL,
@@ -45,47 +48,53 @@
4548
# Default to running every 8 hours, but can be configured with env var
4649
SCHEDULE_HOURS = int(os.getenv("SCHEDULE_HOURS", "8"))
4750

48-
celery_app.conf.beat_schedule = {
49-
"collect-bluesky-data": {
50-
"task": "tasks.collect_and_analyze",
51-
"schedule": crontab(hour=f"*/{SCHEDULE_HOURS}", minute=0),
52-
"options": {"expires": 60 * 60 * 3}, # Tasks expire after 3 hours
53-
},
54-
"generate-alerts": {
55-
"task": "tasks.generate_alerts",
56-
"schedule": 300.0, # every 5 minutes
57-
"options": {
58-
"expires": 60 * 5, # Tasks expire after 5 minutes
51+
# SHOWCASE MODE: All scheduled tasks are disabled to prevent Gemini/Google API costs
52+
# The app displays existing data only - no new data collection or AI analysis
53+
if SHOWCASE_MODE:
54+
print("🎭 SHOWCASE MODE ENABLED - All scheduled tasks disabled")
55+
celery_app.conf.beat_schedule = {}
56+
else:
57+
celery_app.conf.beat_schedule = {
58+
"collect-bluesky-data": {
59+
"task": "tasks.collect_and_analyze",
60+
"schedule": crontab(hour=f"*/{SCHEDULE_HOURS}", minute=0),
61+
"options": {"expires": 60 * 60 * 3}, # Tasks expire after 3 hours
5962
},
60-
},
61-
"manage-alert-queue": {
62-
"task": "tasks.manage_alert_queue",
63-
"schedule": 120.0, # every 2 minutes
64-
"options": {
65-
"expires": 60 * 2, # Tasks expire after 2 minutes
63+
"generate-alerts": {
64+
"task": "tasks.generate_alerts",
65+
"schedule": 300.0, # every 5 minutes
66+
"options": {
67+
"expires": 60 * 5, # Tasks expire after 5 minutes
68+
},
6669
},
67-
},
68-
"send-alert-emails": {
69-
"task": "tasks.send_alert_emails",
70-
"schedule": 120.0, # every 2 minutes
71-
"options": {
72-
"expires": 60 * 2, # Tasks expire after 2 minutes
70+
"manage-alert-queue": {
71+
"task": "tasks.manage_alert_queue",
72+
"schedule": 120.0, # every 2 minutes
73+
"options": {
74+
"expires": 60 * 2, # Tasks expire after 2 minutes
75+
},
7376
},
74-
},
75-
"cleanup-alerts": {
76-
"task": "tasks.cleanup_old_alerts",
77-
"schedule": crontab(hour=2, minute=0), # 2 AM daily
78-
"options": {
79-
"expires": 60 * 60 * 24, # Tasks expire after 24 hours
77+
"send-alert-emails": {
78+
"task": "tasks.send_alert_emails",
79+
"schedule": 120.0, # every 2 minutes
80+
"options": {
81+
"expires": 60 * 2, # Tasks expire after 2 minutes
82+
},
8083
},
81-
},
82-
"archive-completed-disasters": {
83-
"task": "tasks.archive_completed_disasters",
84-
"schedule": crontab(hour=3, minute=0), # 3 AM daily
85-
"options": {
86-
"expires": 60 * 60 * 24, # Tasks expire after 24 hours
84+
"cleanup-alerts": {
85+
"task": "tasks.cleanup_old_alerts",
86+
"schedule": crontab(hour=2, minute=0), # 2 AM daily
87+
"options": {
88+
"expires": 60 * 60 * 24, # Tasks expire after 24 hours
89+
},
8790
},
88-
},
89-
}
91+
"archive-completed-disasters": {
92+
"task": "tasks.archive_completed_disasters",
93+
"schedule": crontab(hour=3, minute=0), # 3 AM daily
94+
"options": {
95+
"expires": 60 * 60 * 24, # Tasks expire after 24 hours
96+
},
97+
},
98+
}
9099

91100
celery_app.conf.timezone = "UTC"

server/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def get_commit_sha():
4848

4949
APP_VERSION = get_version()
5050
COMMIT_SHA = get_commit_sha()
51+
SHOWCASE_MODE = os.getenv("SHOWCASE_MODE", "true").lower() == "true"
5152

5253
app = FastAPI(
5354
title="BlueRelief API",
@@ -125,6 +126,7 @@ async def version_info():
125126
"version": APP_VERSION,
126127
"commit": COMMIT_SHA,
127128
"environment": os.getenv("ENVIRONMENT", "development"),
129+
"showcase_mode": SHOWCASE_MODE,
128130
}
129131

130132

server/routers/admin_tasks.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pydantic import BaseModel
33
from typing import List, Optional
44
from datetime import datetime
5+
import os
56
from db_utils.db import (
67
SessionLocal,
78
User,
@@ -27,6 +28,18 @@
2728

2829
router = APIRouter(prefix="/api/admin/tasks", tags=["admin-tasks"])
2930

31+
# SHOWCASE_MODE: When enabled, blocks all AI/data collection tasks
32+
SHOWCASE_MODE = os.getenv("SHOWCASE_MODE", "true").lower() == "true"
33+
34+
35+
def check_showcase_mode():
36+
"""Raises HTTPException if showcase mode is enabled"""
37+
if SHOWCASE_MODE:
38+
raise HTTPException(
39+
status_code=403,
40+
detail="🎭 Showcase Mode: AI features disabled. This app is in portfolio/demo mode.",
41+
)
42+
3043

3144
def get_db():
3245
db = SessionLocal()
@@ -56,6 +69,7 @@ def trigger_collection(req: CollectRequest, current_admin: User = Depends(get_cu
5669
Accepts JSON body: { "include_enhanced": bool, "disaster_types": ["earthquake","flood"] }
5770
Note: disaster_types is currently accepted for future use but not passed to the Celery task.
5871
"""
72+
check_showcase_mode()
5973
try:
6074
task = collect_and_analyze.delay(include_enhanced=req.include_enhanced)
6175
return {"task_id": task.id, "status": "started"}
@@ -65,6 +79,7 @@ def trigger_collection(req: CollectRequest, current_admin: User = Depends(get_cu
6579

6680
@router.post("/generate-alerts")
6781
def trigger_alert_generation(current_admin: User = Depends(get_current_admin)):
82+
check_showcase_mode()
6883
try:
6984
task = generate_alerts.delay()
7085
return {"task_id": task.id, "status": "started"}
@@ -74,6 +89,7 @@ def trigger_alert_generation(current_admin: User = Depends(get_current_admin)):
7489

7590
@router.post("/process-queue")
7691
def trigger_queue_processing(current_admin: User = Depends(get_current_admin)):
92+
check_showcase_mode()
7793
try:
7894
task = manage_alert_queue.delay()
7995
return {"task_id": task.id, "status": "started"}
@@ -83,6 +99,7 @@ def trigger_queue_processing(current_admin: User = Depends(get_current_admin)):
8399

84100
@router.post("/cleanup-alerts")
85101
def trigger_alert_cleanup(current_admin: User = Depends(get_current_admin)):
102+
check_showcase_mode()
86103
try:
87104
task = cleanup_old_alerts.delay()
88105
return {"task_id": task.id, "status": "started"}
@@ -92,6 +109,7 @@ def trigger_alert_cleanup(current_admin: User = Depends(get_current_admin)):
92109

93110
@router.post("/archive")
94111
def trigger_archive(days_threshold: int = 2, current_admin: User = Depends(get_current_admin)):
112+
check_showcase_mode()
95113
try:
96114
task = archive_completed_disasters.delay(days_threshold=days_threshold)
97115
return {"task_id": task.id, "status": "started", "days_threshold": days_threshold}
@@ -144,6 +162,7 @@ def trigger_test_alert(
144162
Trigger a test alert for a specific user at their location.
145163
Creates a test disaster near the user and queues an alert for them.
146164
"""
165+
check_showcase_mode()
147166
db = SessionLocal()
148167
try:
149168
user = (

server/routers/alerts.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from fastapi import APIRouter, Depends, HTTPException, Query
22
from sqlalchemy.orm import Session
3+
import os
34
from db_utils.db import Alert, AlertQueue, UserAlertPreferences, User, SessionLocal, get_db_session
45
from services.geocoding_service import geocode_region
56
from typing import Optional, List
@@ -8,6 +9,9 @@
89

910
router = APIRouter(prefix="/api/alerts", tags=["alerts"])
1011

12+
# SHOWCASE_MODE: When enabled, disables geocoding API calls
13+
SHOWCASE_MODE = os.getenv("SHOWCASE_MODE", "true").lower() == "true"
14+
1115

1216
class AlertResponse(BaseModel):
1317
id: int
@@ -286,6 +290,13 @@ def update_user_location(
286290
@router.get("/regions/search", response_model=RegionSearchResult)
287291
def search_region(query: str = Query(..., min_length=2)):
288292
"""Search for a region using Google Geocoding API"""
293+
# SHOWCASE MODE: Geocoding disabled to prevent API costs
294+
if SHOWCASE_MODE:
295+
raise HTTPException(
296+
status_code=403,
297+
detail="🎭 Showcase Mode: Region search disabled. This app is in portfolio/demo mode."
298+
)
299+
289300
result = geocode_region(query)
290301

291302
if not result:

server/routers/archive.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from fastapi import APIRouter, Depends, HTTPException
22
from sqlalchemy.orm import Session
3+
import os
34
from db_utils.db import SessionLocal, User
45
from middleware.admin_auth import get_current_admin
56
from tasks import archive_completed_disasters
67
from celery_app import celery_app
78

89
router = APIRouter(prefix="/api/archive", tags=["archive"])
910

11+
# SHOWCASE_MODE: When enabled, blocks task triggers
12+
SHOWCASE_MODE = os.getenv("SHOWCASE_MODE", "true").lower() == "true"
13+
1014

1115
def get_db():
1216
db = SessionLocal()
@@ -43,6 +47,11 @@ def trigger_archive(
4347
Returns:
4448
Task info with ID and status
4549
"""
50+
if SHOWCASE_MODE:
51+
raise HTTPException(
52+
status_code=403,
53+
detail="🎭 Showcase Mode: Task triggers disabled. This app is in portfolio/demo mode."
54+
)
4655
try:
4756
task = archive_completed_disasters.delay(days_threshold=days_threshold)
4857
return {

0 commit comments

Comments
 (0)