Skip to content

Commit 0a2c34d

Browse files
GOOD
1 parent 6a2cb44 commit 0a2c34d

File tree

4 files changed

+530
-18
lines changed

4 files changed

+530
-18
lines changed

src/components/admin/EnhancedAdminDashboard.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import VolunteerManager from './VolunteerManager';
1919
import CampaignManager from './CampaignManager';
2020
import BulkUploadManager from './BulkUploadManager';
2121
import EventManager from './EventManager';
22+
import LegislativeIntelligence from './LegislativeIntelligence';
2223
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell } from 'recharts';
2324

2425
const EnhancedAdminDashboard = () => {
@@ -180,9 +181,13 @@ const EnhancedAdminDashboard = () => {
180181
<TabsTrigger value="events" className="rounded-xl px-6 py-3">Events</TabsTrigger>
181182
<TabsTrigger value="campaigns" className="rounded-xl px-6 py-3">Campaigns</TabsTrigger>
182183
<TabsTrigger value="uploads" className="rounded-xl px-6 py-3">Uploads</TabsTrigger>
183-
<TabsTrigger value="sessions" className="rounded-xl px-6 py-3">Sessions</TabsTrigger>
184-
<TabsTrigger value="changes" className="rounded-xl px-6 py-3">Audit</TabsTrigger>
185-
<TabsTrigger value="settings" className="rounded-xl px-6 py-3">System</TabsTrigger>
184+
<TabsTrigger value="sessions" className="rounded-xl px-2 py-3">Sessions</TabsTrigger>
185+
<TabsTrigger value="intelligence" className="rounded-xl px-2 py-3 gap-1">
186+
<Activity className="h-3 w-3" />
187+
Intel
188+
</TabsTrigger>
189+
<TabsTrigger value="changes" className="rounded-xl px-2 py-3">Audit</TabsTrigger>
190+
<TabsTrigger value="settings" className="rounded-xl px-2 py-3">System</TabsTrigger>
186191
</TabsList>
187192
</div>
188193

@@ -199,44 +204,46 @@ const EnhancedAdminDashboard = () => {
199204
<CardContent>
200205
<div className="text-3xl font-black">{stats.total_users.toLocaleString()}</div>
201206
<p className="text-[10px] text-muted-foreground mt-1 flex items-center gap-1">
202-
<span className="text-kenya-green font-bold">+{stats.recent_signups}</span> new this week
207+
<span className="text-kenya-green font-bold">+{stats.recent_signups}</span> signups
203208
</p>
204209
</CardContent>
205210
</Card>
206-
{/* Repeat for other stats with similar glass-card style */}
211+
207212
<Card className="glass-card border-none shadow-ios-high dark:shadow-ios-high-dark overflow-hidden group">
208-
<div className="absolute top-0 left-0 w-1 h-full bg-kenya-red transition-all group-hover:w-2" />
213+
<div className="absolute top-0 left-0 w-1 h-full bg-primary transition-all group-hover:w-2" />
209214
<CardHeader className="flex flex-row items-center justify-between pb-2">
210-
<CardTitle className="text-sm font-medium">Content Flow</CardTitle>
211-
<BookOpen className="h-4 w-4 text-muted-foreground" />
215+
<CardTitle className="text-sm font-medium">Legislative Trace</CardTitle>
216+
<Activity className="h-4 w-4 text-muted-foreground" />
212217
</CardHeader>
213218
<CardContent>
214-
<div className="text-3xl font-black">{stats.total_posts + stats.total_resources}</div>
219+
<div className="text-3xl font-black">{stats.total_bills}</div>
215220
<p className="text-[10px] text-muted-foreground mt-1 flex items-center gap-1">
216-
<span className="text-kenya-green font-bold">Live Synced</span>
221+
<span className="text-kenya-green font-bold">Live Monitoring</span>
217222
</p>
218223
</CardContent>
219224
</Card>
225+
220226
<Card className="glass-card border-none shadow-ios-high dark:shadow-ios-high-dark overflow-hidden group">
221227
<div className="absolute top-0 left-0 w-1 h-full bg-amber-500 transition-all group-hover:w-2" />
222228
<CardHeader className="flex flex-row items-center justify-between pb-2">
223-
<CardTitle className="text-sm font-medium">System Health</CardTitle>
229+
<CardTitle className="text-sm font-medium">Storage Vault</CardTitle>
224230
<Shield className="h-4 w-4 text-muted-foreground" />
225231
</CardHeader>
226232
<CardContent>
227-
<div className="text-3xl font-black">{stats.active_sessions > 0 ? '99.9%' : 'Checking...'}</div>
228-
<p className="text-[10px] text-muted-foreground mt-1">{stats.active_sessions} active sessions</p>
233+
<div className="text-3xl font-black">B2</div>
234+
<p className="text-[10px] text-muted-foreground mt-1">Private Proxy Active</p>
229235
</CardContent>
230236
</Card>
237+
231238
<Card className="glass-card border-none shadow-ios-high dark:shadow-ios-high-dark overflow-hidden group">
232-
<div className="absolute top-0 left-0 w-1 h-full bg-primary transition-all group-hover:w-2" />
239+
<div className="absolute top-0 left-0 w-1 h-full bg-kenya-red transition-all group-hover:w-2" />
233240
<CardHeader className="flex flex-row items-center justify-between pb-2">
234-
<CardTitle className="text-sm font-medium">Active Assemblies</CardTitle>
235-
<MessageSquare className="h-4 w-4 text-muted-foreground" />
241+
<CardTitle className="text-sm font-medium">Content Flow</CardTitle>
242+
<BookOpen className="h-4 w-4 text-muted-foreground" />
236243
</CardHeader>
237244
<CardContent>
238-
<div className="text-3xl font-black">{stats.total_discussions}</div>
239-
<p className="text-[10px] text-muted-foreground mt-1">{stats.total_interactions} interactions today</p>
245+
<div className="text-3xl font-black">{stats.total_posts + stats.total_resources}</div>
246+
<p className="text-[10px] text-muted-foreground mt-1">Resources & Posts</p>
240247
</CardContent>
241248
</Card>
242249
</>
@@ -407,6 +414,10 @@ const EnhancedAdminDashboard = () => {
407414
<AdminSessionManager />
408415
</TabsContent>
409416

417+
<TabsContent value="intelligence">
418+
<LegislativeIntelligence />
419+
</TabsContent>
420+
410421
<TabsContent value="uploads">
411422
<BulkUploadManager />
412423
</TabsContent>
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
2+
import React, { useState, useEffect } from 'react';
3+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4+
import { Button } from '@/components/ui/button';
5+
import { Input } from '@/components/ui/input';
6+
import { Badge } from '@/components/ui/badge';
7+
import { useToast } from '@/hooks/use-toast';
8+
import {
9+
Globe, Search, Play, Plus, Trash2, RefreshCw,
10+
ExternalLink, CheckCircle2, AlertCircle, Clock, Activity
11+
} from 'lucide-react';
12+
import { supabase } from '@/integrations/supabase/client';
13+
14+
interface ScraperSource {
15+
id: string;
16+
name: string;
17+
url: string;
18+
status: string;
19+
last_scraped_at: string | null;
20+
}
21+
22+
interface ProcessingJob {
23+
id: string;
24+
job_name: string;
25+
status: string;
26+
progress: number;
27+
current_step: string;
28+
created_at: string;
29+
}
30+
31+
const LegislativeIntelligence = () => {
32+
const [sources, setSources] = useState<ScraperSource[]>([]);
33+
const [jobs, setJobs] = useState<ProcessingJob[]>([]);
34+
const [loading, setLoading] = useState(true);
35+
const [newUrl, setNewUrl] = useState('');
36+
const [newName, setNewName] = useState('');
37+
const { toast } = useToast();
38+
39+
useEffect(() => {
40+
fetchData();
41+
const interval = setInterval(fetchData, 10000); // Polling for jobs
42+
return () => clearInterval(interval);
43+
}, []);
44+
45+
const fetchData = async () => {
46+
try {
47+
const { data: sourcesData } = await (supabase
48+
.from('scraper_sources' as any)
49+
.select('*')
50+
.order('created_at', { ascending: false }) as any);
51+
52+
const { data: jobsData } = await (supabase
53+
.from('processing_jobs' as any)
54+
.select('*')
55+
.order('created_at', { ascending: false })
56+
.limit(5) as any);
57+
58+
if (sourcesData) setSources(sourcesData);
59+
if (jobsData) setJobs(jobsData);
60+
} catch (err) {
61+
console.error('Error fetching data:', err);
62+
} finally {
63+
setLoading(false);
64+
}
65+
};
66+
67+
const handleAddSource = async () => {
68+
if (!newUrl || !newName) {
69+
toast({ title: "Error", description: "Name and URL are required", variant: "destructive" });
70+
return;
71+
}
72+
73+
try {
74+
const { error } = await supabase
75+
.from('scraper_sources' as any)
76+
.insert([{ name: newName, url: newUrl }]);
77+
78+
if (error) throw error;
79+
80+
toast({ title: "Success", description: "Scraper source added" });
81+
setNewUrl('');
82+
setNewName('');
83+
fetchData();
84+
} catch (err: any) {
85+
toast({ title: "Error", description: err.message, variant: "destructive" });
86+
}
87+
};
88+
89+
const handleTriggerScrape = async (source: ScraperSource) => {
90+
try {
91+
// Trigger the Edge Function
92+
const { data, error } = await supabase.functions.invoke('process-url', {
93+
body: { url: source.url, data_type: 'bills' }
94+
});
95+
96+
if (error) throw error;
97+
98+
toast({
99+
title: "Crawl Started",
100+
description: `Deployment job for ${source.name} is now in queue.`
101+
});
102+
fetchData();
103+
} catch (err: any) {
104+
toast({ title: "Trigger Failed", description: err.message, variant: "destructive" });
105+
}
106+
};
107+
108+
const getStatusIcon = (status: string) => {
109+
switch (status) {
110+
case 'completed': return <CheckCircle2 className="h-4 w-4 text-kenya-green" />;
111+
case 'processing': return <RefreshCw className="h-4 w-4 text-primary animate-spin" />;
112+
case 'failed': return <AlertCircle className="h-4 w-4 text-kenya-red" />;
113+
default: return <Clock className="h-4 w-4 text-muted-foreground" />;
114+
}
115+
};
116+
117+
return (
118+
<div className="space-y-6">
119+
<div className="grid lg:grid-cols-2 gap-6">
120+
{/* Source Management */}
121+
<Card className="rounded-[32px] border-none shadow-ios-high dark:shadow-ios-high-dark bg-white dark:bg-[#111]">
122+
<CardHeader>
123+
<CardTitle className="flex items-center gap-2">
124+
<Globe className="h-5 w-5 text-primary" />
125+
Scraper Sources
126+
</CardTitle>
127+
<CardDescription>Configure URLs for bill monitoring and trace acquisition</CardDescription>
128+
</CardHeader>
129+
<CardContent className="space-y-6">
130+
<div className="flex flex-col gap-3">
131+
<Input
132+
placeholder="Source Name (e.g. Parliament Portal)"
133+
value={newName}
134+
onChange={(e) => setNewName(e.target.value)}
135+
className="rounded-xl"
136+
/>
137+
<div className="flex gap-2">
138+
<Input
139+
placeholder="https://..."
140+
value={newUrl}
141+
onChange={(e) => setNewUrl(e.target.value)}
142+
className="rounded-xl"
143+
/>
144+
<Button onClick={handleAddSource} className="rounded-xl bg-primary">
145+
<Plus className="h-4 w-4" />
146+
</Button>
147+
</div>
148+
</div>
149+
150+
<div className="space-y-3">
151+
{sources.map(source => (
152+
<div key={source.id} className="p-4 rounded-2xl bg-slate-50 dark:bg-white/5 border border-border/50 flex items-center justify-between group">
153+
<div className="flex flex-col">
154+
<span className="font-bold text-sm">{source.name}</span>
155+
<span className="text-[10px] text-muted-foreground truncate max-w-[200px]">{source.url}</span>
156+
</div>
157+
<div className="flex items-center gap-2">
158+
<Button
159+
onClick={() => handleTriggerScrape(source)}
160+
variant="ghost"
161+
size="sm"
162+
className="rounded-lg hover:bg-kenya-green/10 hover:text-kenya-green"
163+
>
164+
<Play className="h-4 w-4" />
165+
</Button>
166+
<Button variant="ghost" size="sm" asChild className="rounded-lg">
167+
<a href={source.url} target="_blank" rel="noreferrer">
168+
<ExternalLink className="h-4 w-4" />
169+
</a>
170+
</Button>
171+
</div>
172+
</div>
173+
))}
174+
</div>
175+
</CardContent>
176+
</Card>
177+
178+
{/* Live Jobs Monitior */}
179+
<Card className="rounded-[32px] border-none shadow-ios-high dark:shadow-ios-high-dark bg-white dark:bg-[#111]">
180+
<CardHeader>
181+
<CardTitle className="flex items-center gap-2">
182+
<Activity className="h-5 w-5 text-kenya-green" />
183+
Intelligence Queue
184+
</CardTitle>
185+
<CardDescription>Real-time status of neural scraping and analysis jobs</CardDescription>
186+
</CardHeader>
187+
<CardContent>
188+
<div className="space-y-4">
189+
{jobs.length === 0 ? (
190+
<div className="py-12 text-center opacity-40">
191+
<Clock className="h-8 w-8 mx-auto mb-2" />
192+
<p className="text-xs uppercase font-black tracking-widest">No Active Jobs</p>
193+
</div>
194+
) : (
195+
jobs.map(job => (
196+
<div key={job.id} className="space-y-2">
197+
<div className="flex items-center justify-between">
198+
<div className="flex items-center gap-2">
199+
{getStatusIcon(job.status)}
200+
<span className="font-bold text-xs">{job.job_name}</span>
201+
</div>
202+
<Badge variant="outline" className="text-[9px] uppercase tracking-tighter">
203+
{job.status}
204+
</Badge>
205+
</div>
206+
<div className="h-1.5 w-full bg-slate-100 dark:bg-white/5 rounded-full overflow-hidden">
207+
<div
208+
className="h-full bg-primary transition-all duration-500"
209+
style={{ width: `${job.progress}%` }}
210+
/>
211+
</div>
212+
<div className="flex items-center justify-between text-[9px] text-muted-foreground uppercase tracking-widest">
213+
<span>{job.current_step}</span>
214+
<span>{job.progress}%</span>
215+
</div>
216+
</div>
217+
))
218+
)}
219+
</div>
220+
</CardContent>
221+
</Card>
222+
</div>
223+
</div>
224+
);
225+
};
226+
227+
export default LegislativeIntelligence;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
-- ================================================
2+
-- CEKA Legislative Scraper Infrastructure
3+
-- ================================================
4+
5+
-- 1. SCRAPER SOURCES TABLE
6+
CREATE TABLE IF NOT EXISTS public.scraper_sources (
7+
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
8+
name text NOT NULL,
9+
url text NOT NULL,
10+
selector_config jsonb DEFAULT '{}'::jsonb, -- CSS selectors or specific parsing logic
11+
last_scraped_at timestamp with time zone,
12+
status text DEFAULT 'active', -- active, inactive, failing
13+
frequency_hours integer DEFAULT 24,
14+
created_at timestamp with time zone DEFAULT now() NOT NULL,
15+
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL
16+
);
17+
18+
-- 2. ENHANCE PROCESSING JOBS (if not already there)
19+
-- This table tracks the progress of crawls and analysis
20+
CREATE TABLE IF NOT EXISTS public.processing_jobs (
21+
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
22+
job_name text NOT NULL,
23+
input_urls text[] DEFAULT '{}'::text[],
24+
input_files text[] DEFAULT '{}'::text[],
25+
status text DEFAULT 'pending', -- pending, processing, completed, failed
26+
progress integer DEFAULT 0,
27+
current_step text,
28+
result_data jsonb DEFAULT '{}'::jsonb,
29+
error_message text,
30+
processing_logs jsonb DEFAULT '{}'::jsonb,
31+
created_at timestamp with time zone DEFAULT now() NOT NULL,
32+
updated_at timestamp with time zone DEFAULT now(),
33+
completed_at timestamp with time zone
34+
);
35+
36+
-- 3. RLS POLICIES
37+
ALTER TABLE public.scraper_sources ENABLE ROW LEVEL SECURITY;
38+
ALTER TABLE public.processing_jobs ENABLE ROW LEVEL SECURITY;
39+
40+
DROP POLICY IF EXISTS "Admins can manage scraper sources" ON public.scraper_sources;
41+
CREATE POLICY "Admins can manage scraper sources"
42+
ON public.scraper_sources FOR ALL
43+
USING (EXISTS (SELECT 1 FROM public.user_roles WHERE user_id = auth.uid() AND role = 'admin'));
44+
45+
DROP POLICY IF EXISTS "Admins can view processing jobs" ON public.processing_jobs;
46+
CREATE POLICY "Admins can view processing jobs"
47+
ON public.processing_jobs FOR ALL
48+
USING (EXISTS (SELECT 1 FROM public.user_roles WHERE user_id = auth.uid() AND role = 'admin'));
49+
50+
-- 4. SEED SAMPLE SOURCES
51+
INSERT INTO public.scraper_sources (name, url, selector_config)
52+
VALUES
53+
('National Assembly Bills', 'http://www.parliament.go.ke/the-national-assembly/house-business/bills', '{"row_selector": ".views-row", "title_selector": ".bill-title"}'),
54+
('The Senate Bills', 'http://www.parliament.go.ke/the-senate/house-business/bills', '{"row_selector": ".views-row", "title_selector": ".bill-title"}'),
55+
('Kenya Gazette', 'http://kenyalaw.org/kenya_gazette/', '{"row_selector": ".gazette-row", "title_selector": ".gazette-title"}')
56+
ON CONFLICT DO NOTHING;

0 commit comments

Comments
 (0)