Function bodies 140 total
AdminLayout function · typescript · L3-L12 (10 LOC)apps/mobile/app/admin/_layout.tsx
export default function AdminLayout() {
return (
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: '#06060f' },
}}
/>
)
}formatDate function · typescript · L16-L19 (4 LOC)apps/mobile/app/admin/review.tsx
function formatDate(dateStr: string): string {
const d = new Date(dateStr)
return d.toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })
}AdminReviewScreen function · typescript · L21-L232 (212 LOC)apps/mobile/app/admin/review.tsx
export default function AdminReviewScreen() {
const [adminKey, setAdminKey] = useState('')
const [isAuthenticated, setIsAuthenticated] = useState(false)
const queryClient = useQueryClient()
const { data: submissions, isLoading, error, refetch } = useQuery({
queryKey: ['admin-pending', adminKey],
queryFn: () => getPendingSubmissions(adminKey),
enabled: isAuthenticated && adminKey.length > 0,
staleTime: 30 * 1000,
})
const reviewMutation = useMutation({
mutationFn: ({ id, action, note }: { id: string; action: 'approve' | 'reject'; note: string }) =>
reviewSubmission(id, action, note, adminKey),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-pending'] })
},
onError: (err: Error) => {
Alert.alert('Error', err.message)
},
})
const handleReview = useCallback((id: string, action: 'approve' | 'reject') => {
reviewMutation.mutate({ id, action, note: '' })
}, [reviewMutation])
const renderItemRootLayout function · typescript · L17-L29 (13 LOC)apps/mobile/app/_layout.tsx
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<StatusBar style="light" />
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: '#06060f' },
}}
/>
</QueryClientProvider>
)
}HomeScreen function · typescript · L9-L86 (78 LOC)apps/mobile/app/(tabs)/index.tsx
export default function HomeScreen() {
const { areaId, language } = usePreferencesStore()
const [refreshing, setRefreshing] = useState(false)
const { data: brief, isLoading, error, refetch } = useQuery({
queryKey: ['morning-brief', areaId, language],
queryFn: () => getMorningBrief(areaId, language),
enabled: !!areaId,
staleTime: 5 * 60 * 1000,
})
const onRefresh = useCallback(async () => {
setRefreshing(true)
await refetch()
setRefreshing(false)
}, [refetch])
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#06060f' }}>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 16 }}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#6366f1" />
}
>
{/* Header */}
<View style={{ marginBottom: 24 }}>
<Text style={{ color: 'rgba(255,255,255,0.4)', fontSize: 12, textTransform: 'uppercase', letterSpacing: 2 TabLayout function · typescript · L13-L57 (45 LOC)apps/mobile/app/(tabs)/_layout.tsx
export default function TabLayout() {
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: TAB_BAR_STYLE,
tabBarActiveTintColor: '#6366f1',
tabBarInactiveTintColor: 'rgba(255,255,255,0.4)',
tabBarLabelStyle: {
fontSize: 11,
fontWeight: '500',
},
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
}}
/>
<Tabs.Screen
name="map"
options={{
title: 'Map',
tabBarIcon: ({ color, size }) => <Map size={size} color={color} />,
}}
/>
<Tabs.Screen
name="news"
options={{
title: 'News',
tabBarIcon: ({ color, size }) => <Newspaper size={size} color={color} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Settings',
MapScreen function · typescript · L18-L100 (83 LOC)apps/mobile/app/(tabs)/map.tsx
export default function MapScreen() {
const { areaId } = usePreferencesStore()
const [activeLayers, setActiveLayers] = useState<Set<LayerType>>(new Set(['aqi', 'water', 'news']))
const activeLayerArray = Array.from(activeLayers)
// Get current area center
const currentArea = useMemo(
() => PUNE_AREAS.find(a => a.id === areaId),
[areaId]
)
// Fetch data for all active layers
const { data: layerData, isLoading } = useQuery({
queryKey: ['layer-data', areaId, activeLayerArray.sort().join(',')],
queryFn: async () => {
if (activeLayerArray.length === 0) return []
const results = await Promise.all(
activeLayerArray.map((layer) => getLayerData(layer, areaId))
)
// Transform flat API data into map markers
return results.flatMap((items, idx) => {
const layer = activeLayerArray[idx]
return items.map((item: any, i: number) => ({
id: item.id || `${layer}-${i}`,
lat: item.lat || currentArea?.Repobility analyzer · published findings · https://repobility.com
SkeletonCard function · typescript · L16-L42 (27 LOC)apps/mobile/app/(tabs)/news.tsx
function SkeletonCard() {
return (
<View
style={{
borderRadius: 16,
backgroundColor: 'rgba(255,255,255,0.05)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
padding: 16,
marginBottom: 10,
gap: 8,
}}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ width: 70, height: 18, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.06)' }} />
<View style={{ width: 40, height: 14, borderRadius: 6, backgroundColor: 'rgba(255,255,255,0.04)' }} />
</View>
<View style={{ width: '90%', height: 16, borderRadius: 6, backgroundColor: 'rgba(255,255,255,0.06)' }} />
<View style={{ width: '70%', height: 14, borderRadius: 6, backgroundColor: 'rgba(255,255,255,0.04)' }} />
<View style={{ width: '60%', height: 14, borderRadius: 6, backgroundColor: 'rgba(255,255,255,0.04)' }} />
<View style={{ flexDirection: 'row', gap: 8, marginTop:NewsScreen function · typescript · L44-L170 (127 LOC)apps/mobile/app/(tabs)/news.tsx
export default function NewsScreen() {
const { areaId } = usePreferencesStore()
const [activeCategory, setActiveCategory] = useState<NewsCategory | 'All'>('All')
const [refreshing, setRefreshing] = useState(false)
const { data: news, isLoading, error, refetch } = useQuery({
queryKey: ['news', areaId],
queryFn: () => getNews(areaId),
enabled: !!areaId,
staleTime: 5 * 60 * 1000,
})
const filteredNews = (news ?? []).filter((item: NewsItem) =>
activeCategory === 'All' ? true : item.category === activeCategory
)
const onRefresh = useCallback(async () => {
setRefreshing(true)
await refetch()
setRefreshing(false)
}, [refetch])
const renderItem = useCallback(({ item }: { item: NewsItem }) => (
<View style={{ marginHorizontal: 16, marginBottom: 10 }}>
<NewsCard
title={item.title}
summary={item.summary}
source={item.source}
sourceUrl={item.source_url}
reliability={item.reliability}
cSettingsScreen function · typescript · L12-L135 (124 LOC)apps/mobile/app/(tabs)/settings.tsx
export default function SettingsScreen() {
const { areaId, language, setAreaId, setLanguage } = usePreferencesStore()
const phase1Areas = PUNE_AREAS.filter(a => PHASE_1_AREA_IDS.includes(a.id))
const phase2Areas = PUNE_AREAS.filter(a => !PHASE_1_AREA_IDS.includes(a.id))
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#06060f' }}>
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16 }}>
{/* Header */}
<Text style={{ color: '#e2e8f0', fontSize: 24, fontWeight: '700', marginBottom: 24 }}>
Settings
</Text>
{/* Area Selection */}
<Text style={{ color: 'rgba(255,255,255,0.4)', fontSize: 11, textTransform: 'uppercase', letterSpacing: 2, marginBottom: 12 }}>
My Area
</Text>
<View style={{ gap: 8, marginBottom: 24 }}>
{phase1Areas.map(area => (
<TouchableOpacity
key={area.id}
onPress={() => setAreaId(area.id)}
LayerToggle function · typescript · L12-L63 (52 LOC)apps/mobile/components/LayerToggle.tsx
export function LayerToggle({ activeLayers, onToggle }: Props) {
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16, gap: 8, flexDirection: 'row' }}
style={{ flexGrow: 0 }}
>
{LAYERS.map((layer) => {
const config = LAYER_CONFIG[layer]
const active = activeLayers.has(layer)
return (
<TouchableOpacity
key={layer}
onPress={() => onToggle(layer)}
activeOpacity={0.7}
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: active ? config.color + '22' : 'rgba(255,255,255,0.05)',
borderWidth: 1,
borderColor: active ? config.color + '55' : 'rgba(255,255,255,0.1)',
}}
>
MorningBrief function · typescript · L18-L143 (126 LOC)apps/mobile/components/MorningBrief.tsx
export function MorningBrief({ brief }: Props) {
return (
<View style={{ gap: 12 }}>
{/* Water */}
<View style={GLASS}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 18 }}>💧</Text>
<Text style={{ color: '#e2e8f0', fontSize: 15, fontWeight: '600' }}>Water Supply</Text>
</View>
<SourceBadge type="GOV" reliability={85} />
</View>
{brief.water ? (
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, marginTop: 8 }}>
{formatTime(brief.water.start_time)} — {formatTime(brief.water.end_time)}
</Text>
) : (
<Text style={{ color: 'rgba(255,255,255,0.3)', fontSize: 13, marginTop: 8 }}>
No schedule available
</Text>
)}
</View>
{/* Power */}
<View style={GLANewsCard function · typescript · L35-L91 (57 LOC)apps/mobile/components/NewsCard.tsx
export function NewsCard({
title,
summary,
source,
sourceUrl,
reliability,
category,
publishedAt,
}: Props) {
const catColor = CATEGORY_COLORS[category] ?? '#6366f1'
return (
<View style={GLASS}>
{/* Category badge + time */}
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<View
style={{
paddingHorizontal: 10,
paddingVertical: 3,
borderRadius: 10,
backgroundColor: catColor + '1a',
borderWidth: 1,
borderColor: catColor + '33',
}}
>
<Text style={{ color: catColor, fontSize: 11, fontWeight: '600' }}>
{category}
</Text>
</View>
<Text style={{ color: 'rgba(255,255,255,0.3)', fontSize: 11 }}>
{formatRelativeTime(publishedAt)}
</Text>
</View>
{/* Title */}
<Text style={{ color: '#e2e8f0', fontSize: 15, foSourceBadge function · typescript · L10-L35 (26 LOC)apps/mobile/components/SourceBadge.tsx
export function SourceBadge({ type, reliability }: Props) {
const colors = SOURCE_COLORS[type]
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 6,
backgroundColor: colors.bg,
borderWidth: 1,
borderColor: colors.border,
}}
>
<Text style={{ color: colors.text, fontSize: 10, fontWeight: '600', fontFamily: 'monospace' }}>
{type}
</Text>
<Text style={{ color: colors.text, fontSize: 9, fontFamily: 'monospace', opacity: 0.8 }}>
{reliability}%
</Text>
</View>
)
}createMarkerIcon function · typescript · L50-L64 (15 LOC)apps/mobile/components/WebMap.tsx
function createMarkerIcon(layer: LayerType) {
const color = LAYER_COLORS[layer] || '#6366f1'
return L.divIcon({
className: 'sajaag-marker',
html: `<div style="
width: 24px; height: 24px; border-radius: 50%;
background: ${color}; border: 2px solid #fff;
box-shadow: 0 2px 8px ${color}80;
display: flex; align-items: center; justify-content: center;
"></div>`,
iconSize: [24, 24],
iconAnchor: [12, 12],
popupAnchor: [0, -16],
})
}Same scanner, your repo: https://repobility.com — Repobility
createAreaCircle function · typescript · L66-L76 (11 LOC)apps/mobile/components/WebMap.tsx
function createAreaCircle(lat: number, lng: number, radiusKm: number) {
return L.circle([lat, lng], {
radius: radiusKm * 1000,
color: '#6366f1',
fillColor: '#6366f1',
fillOpacity: 0.04,
weight: 1,
opacity: 0.3,
dashArray: '6 4',
})
}WebMap function · typescript · L78-L233 (156 LOC)apps/mobile/components/WebMap.tsx
export function WebMap({ markers, activeLayers, center, zoom = 14 }: WebMapProps) {
const mapRef = useRef<L.Map | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const markersLayerRef = useRef<L.LayerGroup | null>(null)
const mapCenter = center || { lat: 18.5150, lng: 73.9270 } // Magarpatta default
// Initialize map
useEffect(() => {
if (!containerRef.current || mapRef.current) return
const map = L.map(containerRef.current, {
center: [mapCenter.lat, mapCenter.lng],
zoom,
zoomControl: true,
attributionControl: false,
})
// Dark tile layer (CartoDB Dark Matter)
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 19,
}).addTo(map)
// Attribution (bottom-right, subtle)
L.control.attribution({ position: 'bottomright', prefix: false })
.addAttribution('© <a href="https://carto.com">CARTO</a>')
.addTo(map)
// Add 5km radius circle around Magarlifespan function · python · L16-L20 (5 LOC)backend/api/main.py
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
"""Start APScheduler on startup, shut it down on exit."""
scheduler.start()
yield
scheduler.shutdown(wait=False)require_admin function · python · L11-L22 (12 LOC)backend/api/middleware/admin_auth.py
async def require_admin(x_admin_key: str = Header(...)) -> str:
"""FastAPI dependency that enforces admin key authentication.
Usage:
@router.get("/admin/something", dependencies=[Depends(require_admin)])
"""
if x_admin_key != settings.ADMIN_API_KEY:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid admin key",
)
return x_admin_keyget_pending_content function · python · L12-L14 (3 LOC)backend/api/routes/admin.py
async def get_pending_content() -> list:
"""Return all content submissions awaiting moderation. Placeholder."""
return []review_content function · python · L18-L24 (7 LOC)backend/api/routes/admin.py
async def review_content(content_id: str, body: AdminReviewRequest) -> AdminReviewResponse:
"""Approve or reject a content submission."""
return AdminReviewResponse(
id=content_id,
action=body.action,
note=body.note,
)get_alerts function · python · L13-L50 (38 LOC)backend/api/routes/alerts.py
async def get_alerts(area_id: str) -> dict:
"""Return active power and water alerts for the area."""
today = date.today().isoformat()
power: list[dict] = []
water: list[dict] = []
if supabase is None:
return {"power": power, "water": water}
try:
# Power outages for today
result = (
supabase.table("power_outages")
.select("*")
.eq("area_id", area_id)
.eq("date", today)
.order("start_time")
.execute()
)
power = result.data or []
except Exception:
pass
try:
# Water schedules for today
result = (
supabase.table("water_schedules")
.select("*")
.eq("area_id", area_id)
.eq("date", today)
.order("start_time")
.execute()
)
water = result.data or []
except Exception:
pass
return {"power": power, "water": water}_latest function · python · L34-L49 (16 LOC)backend/api/routes/brief.py
def _latest(table: str, area_id: str, limit: int = 1) -> list[dict]:
"""Fetch latest records from a table for the given area."""
if supabase is None:
return []
try:
result = (
supabase.table(table)
.select("*")
.eq("area_id", area_id)
.order("scraped_at", desc=True)
.limit(limit)
.execute()
)
return result.data or []
except Exception:
return []Repobility — the code-quality scanner for AI-generated software · https://repobility.com
get_morning_brief function · python · L53-L179 (127 LOC)backend/api/routes/brief.py
async def get_morning_brief(
area_id: str,
lang: str = Query(default="en", description="Language code"),
) -> MorningBrief:
"""Return the aggregated morning brief for an area."""
today = date.today().isoformat()
# Water
water = None
water_rows = _latest("water_schedules", area_id)
if water_rows:
w = water_rows[0]
water = WaterInfo(
start_time=w.get("start_time", "06:00"),
end_time=w.get("end_time", "09:00"),
source=w.get("source", "PMC"),
reliability=w.get("reliability", 85),
)
# Power
power = None
power_rows = _latest("power_outages", area_id)
if power_rows:
p = power_rows[0]
power = PowerInfo(
start_time=p.get("start_time", "00:00"),
end_time=p.get("end_time", "00:00"),
substation=p.get("substation", ""),
source="MSEDCL",
reliability=75,
)
# AQI
aqi = None
aqi_rows = _submit_content function · python · L13-L26 (14 LOC)backend/api/routes/content.py
async def submit_content(body: ContentSubmission) -> ContentSubmissionResponse:
"""Accept user-submitted content for moderation.
The submission is stored with status 'pending' until an admin
approves or rejects it via the admin review endpoint.
"""
return ContentSubmissionResponse(
id=str(uuid.uuid4()),
content_type=body.content_type,
submitter_name=body.submitter_name,
area_id=body.area_id,
payload=body.payload,
status="pending",
)DealCreate class · python · L12-L17 (6 LOC)backend/api/routes/deals.py
class DealCreate(BaseModel):
title: str
description: str
area_id: str
shop_name: str | None = None
valid_until: datetime | None = NoneDealResponse class · python · L20-L22 (3 LOC)backend/api/routes/deals.py
class DealResponse(DealCreate):
id: str
created_at: datetimeget_deals function · python · L26-L28 (3 LOC)backend/api/routes/deals.py
async def get_deals(area_id: str) -> list:
"""Return active deals for the given area. Placeholder."""
return []create_deal function · python · L32-L38 (7 LOC)backend/api/routes/deals.py
async def create_deal(body: DealCreate) -> DealResponse:
"""Submit a new deal. Returns the deal with a generated id."""
return DealResponse(
**body.model_dump(),
id=str(uuid.uuid4()),
created_at=datetime.now(timezone.utc),
)health_check function · python · L13-L38 (26 LOC)backend/api/routes/health.py
async def health_check() -> dict:
"""Return service health and last scraper run timestamps."""
scrapers: list[dict] = []
if supabase is not None:
try:
result = (
supabase.table("scraper_runs")
.select("scraper_name, status, run_at")
.order("run_at", desc=True)
.limit(10)
.execute()
)
scrapers = result.data or []
except Exception:
# DB not reachable or table doesn't exist yet -- that's fine
pass
from config import settings
return {
"status": "ok",
"db_connected": supabase is not None,
"supabase_url": settings.SUPABASE_URL[:30] + "..." if settings.SUPABASE_URL else "not set",
"scrapers": scrapers,
"timestamp": datetime.now(timezone.utc).isoformat(),
}get_layer_data function · python · L39-L70 (32 LOC)backend/api/routes/layers.py
async def get_layer_data(layer_type: str, area_id: str) -> list[dict]:
"""Return geo-features for a given layer type and area."""
if supabase is None or layer_type not in LAYER_TABLES:
return []
config = LAYER_TABLES[layer_type]
try:
query = (
supabase.table(config["table"])
.select(config["fields"])
.limit(50)
)
# News uses array contains, others use eq
if layer_type == "news":
query = query.or_(
f"area_ids.cs.{{{area_id}}},area_ids.cs.{{pune}}"
)
else:
query = query.eq("area_id", area_id)
# Order by most recent
order_col = "scraped_at" if layer_type not in ("news", "deals") else (
"published_at" if layer_type == "news" else "created_at"
)
query = query.order(order_col, desc=True)
result = query.execute()
return result.data or []
except Exception:
return []Source: Repobility analyzer · https://repobility.com
get_news function · python · L11-L39 (29 LOC)backend/api/routes/news.py
async def get_news(
area_id: str,
category: str | None = Query(default=None, description="Filter by category"),
limit: int = Query(default=20, ge=1, le=50),
) -> list[dict]:
"""Return recent local news items for the given area."""
if supabase is None:
return []
try:
query = (
supabase.table("news_items")
.select("*")
.order("published_at", desc=True)
.limit(limit)
)
# Filter by area (items containing area_id or "pune" in area_ids array)
query = query.or_(
f"area_ids.cs.{{{area_id}}},area_ids.cs.{{pune}}"
)
if category:
query = query.eq("category", category)
result = query.execute()
return result.data or []
except Exception:
return []Settings class · python · L6-L39 (34 LOC)backend/config.py
class Settings(BaseSettings):
"""Central configuration for the Sajaag backend.
All values can be overridden via environment variables or a .env file
located in the backend/ directory.
"""
# Supabase
SUPABASE_URL: str = ""
SUPABASE_KEY: str = ""
# Admin
ADMIN_API_KEY: str = "change-me"
# External service URLs
SAFAR_BASE_URL: str = "https://safar.tropmet.res.in"
OPEN_METEO_URL: str = "https://api.open-meteo.com/v1/forecast"
# CORS
CORS_ORIGINS: list[str] = [
"http://localhost:3000",
"http://localhost:5173",
"http://localhost:8081",
"http://localhost:8100",
"http://localhost:19006",
"https://sajaag-web.onrender.com",
"https://sajaag.onrender.com",
]
model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8",
"case_sensitive": True,
}get_supabase function · python · L8-L12 (5 LOC)backend/db.py
def get_supabase() -> Client | None:
"""Return an initialised Supabase client, or None when credentials are missing."""
if not settings.SUPABASE_URL or not settings.SUPABASE_KEY:
return None
return create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)WaterInfo class · python · L9-L13 (5 LOC)backend/models/brief.py
class WaterInfo(BaseModel):
start_time: str
end_time: str
source: str = "PMC"
reliability: int = 85PowerInfo class · python · L15-L20 (6 LOC)backend/models/brief.py
class PowerInfo(BaseModel):
start_time: str
end_time: str
substation: str = ""
source: str = "MSEDCL"
reliability: int = 75AqiInfo class · python · L22-L29 (8 LOC)backend/models/brief.py
class AqiInfo(BaseModel):
aqi: int
pm25: Optional[float] = None
pm10: Optional[float] = None
category: str
trend: str = "stable"
source: str = "SAFAR"
reliability: int = 85WeatherInfo class · python · L31-L37 (7 LOC)backend/models/brief.py
class WeatherInfo(BaseModel):
temp: float
humidity: int
condition: str
rain_probability: int = 0
source: str = "Open-Meteo"
reliability: int = 95NewsItemBrief class · python · L39-L47 (9 LOC)backend/models/brief.py
class NewsItemBrief(BaseModel):
id: str
title: str
summary: str = ""
category: str = "Essential"
source: str = ""
source_url: str = ""
reliability: int = 85
published_at: Optional[str] = NoneRepobility analyzer · published findings · https://repobility.com
DealBrief class · python · L49-L53 (5 LOC)backend/models/brief.py
class DealBrief(BaseModel):
id: str
business_name: str
description: str
discount: Optional[str] = NoneTrafficAlert class · python · L55-L58 (4 LOC)backend/models/brief.py
class TrafficAlert(BaseModel):
id: str
description: str
severity: str = "low"MorningBrief class · python · L60-L71 (12 LOC)backend/models/brief.py
class MorningBrief(BaseModel):
"""Aggregated morning brief for a given area."""
area: str
area_name: str
date: str
water: Optional[WaterInfo] = None
power: Optional[PowerInfo] = None
aqi: Optional[AqiInfo] = None
weather: Optional[WeatherInfo] = None
nearby_news: list[NewsItemBrief] = []
nearby_deals: list[DealBrief] = []
traffic_alerts: list[TrafficAlert] = []ContentSubmission class · python · L9-L16 (8 LOC)backend/models/content.py
class ContentSubmission(BaseModel):
"""Payload accepted by POST /api/content/submit."""
content_type: str # "deal" | "event" | "alert" | "tip"
submitter_name: str
submitter_phone: str
area_id: str
payload: dict[str, Any]ContentSubmissionResponse class · python · L19-L28 (10 LOC)backend/models/content.py
class ContentSubmissionResponse(BaseModel):
"""Returned after a successful submission."""
id: str
content_type: str
submitter_name: str
area_id: str
payload: dict[str, Any]
status: str = "pending"
created_at: datetime = Field(default_factory=datetime.utcnow)AdminReviewRequest class · python · L31-L35 (5 LOC)backend/models/content.py
class AdminReviewRequest(BaseModel):
"""Payload for POST /api/admin/review/{id}."""
action: str # "approve" | "reject"
note: str = ""AdminReviewResponse class · python · L38-L44 (7 LOC)backend/models/content.py
class AdminReviewResponse(BaseModel):
"""Returned after an admin reviews content."""
id: str
action: str
note: str
reviewed_at: datetime = Field(default_factory=datetime.utcnow)categorize_aqi function · python · L44-L50 (7 LOC)backend/scrapers/aqicn_aqi.py
def categorize_aqi(aqi: int) -> str:
if aqi <= 50: return "Good"
if aqi <= 100: return "Satisfactory"
if aqi <= 200: return "Moderate"
if aqi <= 300: return "Poor"
if aqi <= 400: return "Very Poor"
return "Severe"Same scanner, your repo: https://repobility.com — Repobility
AqicnAqiScraper class · python · L53-L121 (69 LOC)backend/scrapers/aqicn_aqi.py
class AqicnAqiScraper(BaseScraper):
name = "aqicn_aqi"
max_retries = 3
async def fetch(self) -> Any:
"""Fetch AQI from all Pune AQICN stations."""
results = {}
async with aiohttp.ClientSession() as session:
for station_id, info in PUNE_STATIONS.items():
try:
url = f"https://api.waqi.info/feed/{station_id}/?token={AQICN_TOKEN}"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status == 200:
data = await resp.json()
if data.get("status") == "ok":
results[station_id] = data["data"]
except Exception as e:
logger.warning("AQICN station %s failed: %s", station_id, e)
if not results:
raise RuntimeError("No AQICN stations returned data")
logger.info("aqicn_aqi: fetched %d/%d statifetch method · python · L57-L75 (19 LOC)backend/scrapers/aqicn_aqi.py
async def fetch(self) -> Any:
"""Fetch AQI from all Pune AQICN stations."""
results = {}
async with aiohttp.ClientSession() as session:
for station_id, info in PUNE_STATIONS.items():
try:
url = f"https://api.waqi.info/feed/{station_id}/?token={AQICN_TOKEN}"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status == 200:
data = await resp.json()
if data.get("status") == "ok":
results[station_id] = data["data"]
except Exception as e:
logger.warning("AQICN station %s failed: %s", station_id, e)
if not results:
raise RuntimeError("No AQICN stations returned data")
logger.info("aqicn_aqi: fetched %d/%d stations", len(results), len(PUNE_STATIONS))
return resultsvalidate method · python · L77-L114 (38 LOC)backend/scrapers/aqicn_aqi.py
async def validate(self, raw: Any) -> list[dict]:
"""Parse AQICN responses into AQI readings per area."""
readings = []
now = datetime.now(timezone.utc).isoformat()
seen_areas = set()
for station_id, data in raw.items():
aqi_val = data.get("aqi")
if not isinstance(aqi_val, (int, float)) or aqi_val < 0:
continue
aqi_val = int(aqi_val)
# Extract PM values from iaqi
iaqi = data.get("iaqi", {})
pm25 = iaqi.get("pm25", {}).get("v")
pm10 = iaqi.get("pm10", {}).get("v")
station_info = PUNE_STATIONS[station_id]
# Find all areas mapped to this station
for area_id, mapped_station in AREA_STATION_MAP.items():
if mapped_station == station_id and area_id not in seen_areas:
seen_areas.add(area_id)
readings.append({
"station_id": station_infpage 1 / 3next ›