← back to drbhatiasanjay__sajaag

Function bodies 140 total

All specs Real LLM only Function bodies
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 renderItem
RootLayout 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}
        c
SettingsScreen 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={GLA
NewsCard 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, fo
SourceBadge 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('&copy; <a href="https://carto.com">CARTO</a>')
      .addTo(map)

    // Add 5km radius circle around Magar
lifespan 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_key
get_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 = None
DealResponse class · python · L20-L22 (3 LOC)
backend/api/routes/deals.py
class DealResponse(DealCreate):
    id: str
    created_at: datetime
get_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 = 85
PowerInfo 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 = 75
AqiInfo 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 = 85
WeatherInfo 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 = 95
NewsItemBrief 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] = None
Repobility 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] = None
TrafficAlert 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 stati
fetch 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 results
validate 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_inf
page 1 / 3next ›