diff --git a/plugins/core/src/db/index.ts b/plugins/core/src/db/index.ts index e9013ce..a3188c3 100644 --- a/plugins/core/src/db/index.ts +++ b/plugins/core/src/db/index.ts @@ -8,6 +8,7 @@ let dbInstance: Database.Database | null = null; export * from './repositories/SiteRepository.js'; export * from './repositories/SnapshotRepository.js'; +export { initSchema } from './schema.js'; export function getDbPath(): string { if (process.env.NODE_ENV === 'test') { diff --git a/plugins/core/src/db/repositories/SnapshotRepository.ts b/plugins/core/src/db/repositories/SnapshotRepository.ts index e39fe42..29eaf97 100644 --- a/plugins/core/src/db/repositories/SnapshotRepository.ts +++ b/plugins/core/src/db/repositories/SnapshotRepository.ts @@ -87,6 +87,10 @@ export class SnapshotRepository { // Unlink pages from this snapshot to prevent FK constraint violations or data inconsistencies this.db.prepare('UPDATE pages SET first_seen_snapshot_id = NULL WHERE first_seen_snapshot_id = ?').run(id); this.db.prepare('UPDATE pages SET last_seen_snapshot_id = NULL WHERE last_seen_snapshot_id = ?').run(id); + + // Cleanup: Delete pages that are no longer referenced by any snapshot + this.db.prepare('DELETE FROM pages WHERE first_seen_snapshot_id IS NULL AND last_seen_snapshot_id IS NULL').run(); + // Delete the snapshot this.db.prepare('DELETE FROM snapshots WHERE id = ?').run(id); }); diff --git a/plugins/server/src/index.ts b/plugins/server/src/index.ts index 3404dc3..c48213b 100644 --- a/plugins/server/src/index.ts +++ b/plugins/server/src/index.ts @@ -332,6 +332,193 @@ export function startServer(options: ServerOptions): Promise { res.json({ results: rows }); }); + // 4.8 GET /api/history (List of snapshots with key stats) + api.get('/history', (req, res) => { + // Fetch snapshots with summary data from the snapshots table. + // Note: Some stats might be null if not computed, but usually they are present. + const sql = ` + SELECT + id, + created_at as createdAt, + node_count as pages, + health_score as health, + orphan_count as orphanPages, + thin_content_count as thinContent + FROM snapshots + WHERE site_id = ? + ORDER BY created_at DESC + `; + const snapshots = db.prepare(sql).all(siteId); + res.json({ results: snapshots }); + }); + + // 4.9 GET /api/history/trends + api.get('/history/trends', (req, res) => { + // Return a time-series list of snapshots with key metrics. + // We'll need to aggregate metrics if they aren't fully in the snapshots table. + // For performance, we'll join snapshots with aggregates from metrics/pages if needed. + // But for now, let's rely on what we have in snapshots table + some fast aggregates. + // Actually, 'broken links' is not in snapshots table directly. We need to count. + + const snapshots = db.prepare(` + SELECT id, created_at, node_count, health_score, orphan_count + FROM snapshots + WHERE site_id = ? + ORDER BY created_at ASC + `).all(siteId) as any[]; + + // Enrich with broken links count for each snapshot (expensive if many snapshots, but tolerable for < 100) + // Optimization: One query to group by snapshot_id + const brokenLinksCounts = db.prepare(` + SELECT m.snapshot_id, COUNT(*) as count + FROM metrics m + JOIN pages p ON m.page_id = p.id + WHERE p.site_id = ? AND ( + p.http_status >= 400 OR + (p.http_status = 0 AND m.crawl_status IN ('network_error', 'failed_after_retries', 'fetched_error')) OR + p.security_error IS NOT NULL + ) + GROUP BY m.snapshot_id + `).all(siteId) as any[]; + + const brokenMap = new Map(brokenLinksCounts.map(r => [r.snapshot_id, r.count])); + + const duplicateClustersCounts = db.prepare(` + SELECT snapshot_id, COUNT(*) as count FROM duplicate_clusters GROUP BY snapshot_id + `).all() as any[]; + + const dupMap = new Map(duplicateClustersCounts.map(r => [r.snapshot_id, r.count])); + + const trends = snapshots.map(snap => ({ + id: snap.id, + date: snap.created_at, + pages: snap.node_count, + health: snap.health_score, + orphans: snap.orphan_count || 0, + brokenLinks: brokenMap.get(snap.id) || 0, + duplicateClusters: dupMap.get(snap.id) || 0 + })); + + res.json({ results: trends }); + }); + + // 4.10 GET /api/history/compare + api.get('/history/compare', (req, res) => { + const { snapshotA, snapshotB } = req.query; + + if (!snapshotA || !snapshotB) { + return res.status(400).json({ error: 'snapshotA and snapshotB are required' }); + } + + const idA = parseInt(snapshotA as string, 10); + const idB = parseInt(snapshotB as string, 10); + + // Verify ownership + const snapA = snapshotRepo.getSnapshot(idA); + const snapB = snapshotRepo.getSnapshot(idB); + + if (!snapA || snapA.site_id !== siteId || !snapB || snapB.site_id !== siteId) { + return res.status(404).json({ error: 'Snapshots not found' }); + } + + // 1. Pages Added (Present in B, not in A) - Check by URL + const addedPagesCount = db.prepare(` + SELECT COUNT(*) as count + FROM pages pB + JOIN metrics mB ON pB.id = mB.page_id AND mB.snapshot_id = ? + WHERE NOT EXISTS ( + SELECT 1 FROM pages pA + JOIN metrics mA ON pA.id = mA.page_id AND mA.snapshot_id = ? + WHERE pA.normalized_url = pB.normalized_url + ) + `).get(idB, idA) as { count: number }; + + // 2. Pages Removed (Present in A, not in B) + const removedPagesCount = db.prepare(` + SELECT COUNT(*) as count + FROM pages pA + JOIN metrics mA ON pA.id = mA.page_id AND mA.snapshot_id = ? + WHERE NOT EXISTS ( + SELECT 1 FROM pages pB + JOIN metrics mB ON pB.id = mB.page_id AND mB.snapshot_id = ? + WHERE pB.normalized_url = pA.normalized_url + ) + `).get(idA, idB) as { count: number }; + + // 3. New Issues (Broken links in B that were OK or non-existent in A) + // "Broken" def: status >= 400 OR network error + const newBrokenLinks = db.prepare(` + SELECT pB.normalized_url + FROM pages pB + JOIN metrics mB ON pB.id = mB.page_id AND mB.snapshot_id = ? + WHERE (pB.http_status >= 400 OR mB.crawl_status IN ('network_error', 'fetched_error')) + AND NOT EXISTS ( + SELECT 1 FROM pages pA + JOIN metrics mA ON pA.id = mA.page_id AND mA.snapshot_id = ? + WHERE pA.normalized_url = pB.normalized_url + AND (pA.http_status >= 400 OR mA.crawl_status IN ('network_error', 'fetched_error')) + ) + LIMIT 50 + `).all(idB, idA) as any[]; + + // 4. Resolved Issues (Broken in A, OK in B) + const resolvedBrokenLinks = db.prepare(` + SELECT pA.normalized_url + FROM pages pA + JOIN metrics mA ON pA.id = mA.page_id AND mA.snapshot_id = ? + WHERE (pA.http_status >= 400 OR mA.crawl_status IN ('network_error', 'fetched_error')) + AND EXISTS ( + SELECT 1 FROM pages pB + JOIN metrics mB ON pB.id = mB.page_id AND mB.snapshot_id = ? + WHERE pB.normalized_url = pA.normalized_url + AND (pB.http_status >= 200 AND pB.http_status < 400 AND mB.crawl_status = 'fetched') + ) + LIMIT 50 + `).all(idA, idB) as any[]; + + // Health Delta + const healthDelta = (snapB.health_score || 0) - (snapA.health_score || 0); + + res.json({ + snapshotA: { id: idA, date: snapA.created_at, health: snapA.health_score, pages: snapA.node_count }, + snapshotB: { id: idB, date: snapB.created_at, health: snapB.health_score, pages: snapB.node_count }, + diff: { + pagesAdded: addedPagesCount.count, + pagesRemoved: removedPagesCount.count, + healthDelta, + newIssues: { + brokenLinks: newBrokenLinks + }, + resolvedIssues: { + brokenLinks: resolvedBrokenLinks + } + } + }); + }); + + // 4.11 DELETE /api/history/:id + api.delete('/history/:id', (req, res) => { + const id = parseInt(req.params.id, 10); + const snap = snapshotRepo.getSnapshot(id); + + if (!snap || snap.site_id !== siteId) { + return res.status(404).json({ error: 'Snapshot not found' }); + } + + // Check if it's the ONLY snapshot + if (snapshotRepo.getSnapshotCount(siteId) <= 1) { + return res.status(400).json({ error: 'Cannot delete the only snapshot.' }); + } + + try { + snapshotRepo.deleteSnapshot(id); + res.json({ success: true }); + } catch (e) { + console.error(e); + res.status(500).json({ error: 'Failed to delete snapshot' }); + } + }); + app.use(API_PREFIX, api); diff --git a/plugins/server/tests/server.test.ts b/plugins/server/tests/server.test.ts index e2eb29e..80d93fa 100644 --- a/plugins/server/tests/server.test.ts +++ b/plugins/server/tests/server.test.ts @@ -23,6 +23,7 @@ vi.mock('express', () => { expressMock.static = vi.fn(); expressMock.Router = vi.fn(() => ({ get: vi.fn(), + delete: vi.fn(), use: vi.fn() })); return { diff --git a/plugins/web/src/App.tsx b/plugins/web/src/App.tsx index 503f5c4..117fcb7 100644 --- a/plugins/web/src/App.tsx +++ b/plugins/web/src/App.tsx @@ -8,6 +8,7 @@ import { SecondaryMetricCard } from './components/Metrics/SecondaryMetricCard'; import { IssuesTable } from './components/IssuesTable'; import { CriticalPanel } from './components/CriticalPanel'; import { GraphIntelligenceSection } from './components/GraphIntelligenceSection'; +import { HistoryView } from './components/History/HistoryView'; import * as API from './api'; export const DashboardContext = React.createContext<{ @@ -27,6 +28,7 @@ export const DashboardContext = React.createContext<{ function App() { const [sidebarOpen, setSidebarOpen] = useState(false); const [showCompare, setShowCompare] = useState(false); + const [currentView, setCurrentView] = useState('dashboard'); // Data State const [loading, setLoading] = useState(true); @@ -113,7 +115,12 @@ function App() { domain: context?.domain || 'Loading...' }}>
- +
setSidebarOpen(!sidebarOpen)} @@ -124,40 +131,50 @@ function App() {
- {/* Primary Metrics Row */} -
- - - -
- - {/* Secondary Metrics Row */} -
- {secondaryMetrics.map((metric, index) => ( - - ))} -
- - {/* Main Section */} -
-
- -
-
- + {currentView === 'history' ? ( + + ) : currentView === 'dashboard' ? ( + <> + {/* Primary Metrics Row */} +
+ + + +
+ + {/* Secondary Metrics Row */} +
+ {secondaryMetrics.map((metric, index) => ( + + ))} +
+ + {/* Main Section */} +
+
+ +
+
+ +
+
+ + {/* Lower Section: Graph Intelligence */} + + + ) : ( +
+ View "{currentView}" not implemented yet.
-
- - {/* Lower Section: Graph Intelligence */} - + )}
diff --git a/plugins/web/src/api.ts b/plugins/web/src/api.ts index 81361fb..3916bc9 100644 --- a/plugins/web/src/api.ts +++ b/plugins/web/src/api.ts @@ -62,6 +62,36 @@ export interface TopPage { export interface Snapshot { id: number; createdAt: string; + pages?: number; + health?: number; + orphanPages?: number; + thinContent?: number; +} + +export interface HistoryTrend { + id: number; + date: string; + pages: number; + health: number; + orphans: number; + brokenLinks: number; + duplicateClusters: number; +} + +export interface SnapshotComparison { + snapshotA: { id: number, date: string, health: number, pages: number }; + snapshotB: { id: number, date: string, health: number, pages: number }; + diff: { + pagesAdded: number; + pagesRemoved: number; + healthDelta: number; + newIssues: { + brokenLinks: { normalized_url: string }[]; + }; + resolvedIssues: { + brokenLinks: { normalized_url: string }[]; + }; + }; } export async function fetchOverview(snapshotId?: number): Promise { @@ -115,3 +145,29 @@ export async function fetchContext(): Promise<{ siteId: number, snapshotId: numb if (!res.ok) throw new Error('Failed to fetch context'); return res.json(); } + +export async function fetchHistory(): Promise<{ results: Snapshot[] }> { + const res = await fetch(`${API_PREFIX}/history`); + if (!res.ok) throw new Error('Failed to fetch history'); + return res.json(); +} + +export async function fetchHistoryTrends(): Promise<{ results: HistoryTrend[] }> { + const res = await fetch(`${API_PREFIX}/history/trends`); + if (!res.ok) throw new Error('Failed to fetch history trends'); + return res.json(); +} + +export async function fetchSnapshotComparison(snapshotA: number, snapshotB: number): Promise { + const res = await fetch(`${API_PREFIX}/history/compare?snapshotA=${snapshotA}&snapshotB=${snapshotB}`); + if (!res.ok) throw new Error('Failed to fetch snapshot comparison'); + return res.json(); +} + +export async function deleteSnapshot(id: number): Promise { + const res = await fetch(`${API_PREFIX}/history/${id}`, { method: 'DELETE' }); + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Failed to delete snapshot'); + } +} diff --git a/plugins/web/src/components/History/ComparisonView.tsx b/plugins/web/src/components/History/ComparisonView.tsx new file mode 100644 index 0000000..4f8d61d --- /dev/null +++ b/plugins/web/src/components/History/ComparisonView.tsx @@ -0,0 +1,174 @@ +import React, { useEffect, useState } from 'react'; +import * as API from '../../api'; +import { ArrowLeft, ArrowRight, AlertTriangle, CheckCircle, FilePlus, FileMinus } from 'lucide-react'; + +interface ComparisonViewProps { + snapshotA: number; + snapshotB: number; + allSnapshots: API.Snapshot[]; + onBack: () => void; + onChangeA: (id: number) => void; + onChangeB: (id: number) => void; +} + +export const ComparisonView: React.FC = ({ snapshotA, snapshotB, allSnapshots, onBack, onChangeA, onChangeB }) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetch = async () => { + setLoading(true); + try { + const res = await API.fetchSnapshotComparison(snapshotA, snapshotB); + setData(res); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + if (snapshotA && snapshotB) fetch(); + }, [snapshotA, snapshotB]); + + if (loading) return
Analyzing differences...
; + if (!data) return
Failed to load comparison.
; + + return ( +
+ {/* Header / Controls */} +
+ + +
+
+ Baseline (A) + +
+ +
+ Comparison (B) + +
+
+
+ + {/* Summary Cards */} +
+
+
+
+ +
+ Pages Added +
+ +{data.diff.pagesAdded} +
+ +
+
+
+ +
+ Pages Removed +
+ -{data.diff.pagesRemoved} +
+ +
+
+
+ +
+ New Issues +
+ + {data.diff.newIssues.brokenLinks.length} + + Broken Links +
+ +
+
+
+ +
+ Health Delta +
+
+ = 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}> + {data.diff.healthDelta > 0 ? '+' : ''}{data.diff.healthDelta.toFixed(1)} + + points +
+
+
+ + {/* Detailed Lists */} +
+ {/* New Issues List */} +
+
+

+ New Broken Links +

+ + {data.diff.newIssues.brokenLinks.length} found + +
+
+ {data.diff.newIssues.brokenLinks.length === 0 ? ( +
No new broken links detected.
+ ) : ( +
    + {data.diff.newIssues.brokenLinks.map((issue, i) => ( +
  • + {issue.normalized_url} +
  • + ))} +
+ )} +
+
+ + {/* Resolved Issues List */} +
+
+

+ Resolved Broken Links +

+ + {data.diff.resolvedIssues.brokenLinks.length} fixed + +
+
+ {data.diff.resolvedIssues.brokenLinks.length === 0 ? ( +
No resolved issues found.
+ ) : ( +
    + {data.diff.resolvedIssues.brokenLinks.map((issue, i) => ( +
  • + {issue.normalized_url} +
  • + ))} +
+ )} +
+
+
+
+ ); +}; diff --git a/plugins/web/src/components/History/HistoryHeader.tsx b/plugins/web/src/components/History/HistoryHeader.tsx new file mode 100644 index 0000000..71aad9f --- /dev/null +++ b/plugins/web/src/components/History/HistoryHeader.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Snapshot } from '../../api'; +import { Calendar, Layers, Clock } from 'lucide-react'; + +interface HistoryHeaderProps { + totalSnapshots: number; + firstSnapshot?: Snapshot; + lastSnapshot?: Snapshot; +} + +export const HistoryHeader: React.FC = ({ totalSnapshots, firstSnapshot, lastSnapshot }) => { + return ( +
+
+
+

+ + Crawl History +

+

+ Track structural evolution, compare snapshots, and analyze trends over time. +

+
+ +
+
+ Total Snapshots + {totalSnapshots} +
+ +
+ +
+ + First Crawl + + + {firstSnapshot ? new Date(firstSnapshot.createdAt).toLocaleDateString() : '-'} + +
+ +
+ + Latest Crawl + + + {lastSnapshot ? new Date(lastSnapshot.createdAt).toLocaleDateString() : '-'} + +
+
+
+
+ ); +}; diff --git a/plugins/web/src/components/History/HistoryView.tsx b/plugins/web/src/components/History/HistoryView.tsx new file mode 100644 index 0000000..69ec0cf --- /dev/null +++ b/plugins/web/src/components/History/HistoryView.tsx @@ -0,0 +1,125 @@ +import React, { useState, useEffect } from 'react'; +import * as API from '../../api'; +import { HistoryHeader } from './HistoryHeader'; +import { TrendChart } from './TrendChart'; +import { SnapshotTable } from './SnapshotTable'; +import { ComparisonView } from './ComparisonView'; + +export const HistoryView: React.FC = () => { + const [loading, setLoading] = useState(true); + const [history, setHistory] = useState([]); + const [trends, setTrends] = useState([]); + const [compareMode, setCompareMode] = useState(false); + const [selectedSnapshotA, setSelectedSnapshotA] = useState(null); + const [selectedSnapshotB, setSelectedSnapshotB] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setLoading(true); + try { + const historyRes = await API.fetchHistory(); + setHistory(historyRes.results); + const trendsRes = await API.fetchHistoryTrends(); + setTrends(trendsRes.results); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id: number) => { + if (!window.confirm('Are you sure you want to delete this snapshot? This action cannot be undone.')) { + return; + } + try { + await API.deleteSnapshot(id); + loadData(); + } catch (e: any) { + alert(`Error: ${e.message}`); + } + }; + + const handleCompare = (snapshotId: number) => { + if (selectedSnapshotA === null) { + setSelectedSnapshotA(snapshotId); + // If we have at least one previous snapshot, auto-select the immediate previous one for B? + // Or just wait for user to select B. + // Actually, UX: click 'Compare' on a row -> sets it as B (current/newer), then prompts to select A (baseline). + // Let's assume the user clicked 'Compare' on the NEWER snapshot they want to analyze. + // So set B = snapshotId. + setSelectedSnapshotB(snapshotId); + + // Try to find the snapshot immediately before this one in the history list (history is sorted DESC) + const currentIndex = history.findIndex(h => h.id === snapshotId); + if (currentIndex !== -1 && currentIndex < history.length - 1) { + setSelectedSnapshotA(history[currentIndex + 1].id); + } else { + // If it's the oldest, maybe clear A so they have to pick + setSelectedSnapshotA(null); + } + setCompareMode(true); + } else { + // If already in compare mode, maybe just update one of them? + // Let's keep it simple: Reset if they click compare again from the list, or handle inside ComparisonView. + setSelectedSnapshotB(snapshotId); + setCompareMode(true); + } + }; + + if (loading) return
Loading History...
; + + if (compareMode && selectedSnapshotA && selectedSnapshotB) { + return ( + setCompareMode(false)} + onChangeA={setSelectedSnapshotA} + onChangeB={setSelectedSnapshotB} + /> + ); + } + + return ( +
+ + + {/* Charts Section */} +
+
+

Total Pages

+ +
+
+

Health Score

+ +
+
+

Broken Links

+ +
+
+ + {/* Snapshots Table */} +
+
+

Snapshots

+
+ +
+
+ ); +}; diff --git a/plugins/web/src/components/History/SnapshotTable.tsx b/plugins/web/src/components/History/SnapshotTable.tsx new file mode 100644 index 0000000..38a9ed8 --- /dev/null +++ b/plugins/web/src/components/History/SnapshotTable.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Snapshot } from '../../api'; +import { Trash2, GitCompare } from 'lucide-react'; + +interface SnapshotTableProps { + snapshots: Snapshot[]; + onDelete: (id: number) => void; + onCompare: (id: number) => void; +} + +export const SnapshotTable: React.FC = ({ snapshots, onDelete, onCompare }) => { + return ( +
+ + + + + + + + + + + + + + {snapshots.map((snap) => ( + + + + + + + + + + ))} + {snapshots.length === 0 && ( + + + + )} + +
IDDatePagesHealthOrphansThin ContentActions
#{snap.id} + {new Date(snap.createdAt).toLocaleString()} + {snap.pages} + 80 ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : + (snap.health || 0) > 50 ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' : + 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'}`}> + {snap.health ?? '-'} + + {snap.orphanPages ?? '-'}{snap.thinContent ?? '-'} + + +
+ No snapshots found. Run a crawl to generate history. +
+
+ ); +}; diff --git a/plugins/web/src/components/History/TrendChart.tsx b/plugins/web/src/components/History/TrendChart.tsx new file mode 100644 index 0000000..738941a --- /dev/null +++ b/plugins/web/src/components/History/TrendChart.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { HistoryTrend } from '../../api'; + +interface TrendChartProps { + data: HistoryTrend[]; + dataKey: keyof HistoryTrend; + color?: string; + height?: number; + label?: string; +} + +export const TrendChart: React.FC = ({ data, dataKey, color = '#3b82f6', height = 200, label }) => { + if (!data || data.length < 2) { + return ( +
+ Not enough data to display trend. +
+ ); + } + + const values = data.map(d => Number(d[dataKey])); + const min = Math.min(...values); + const max = Math.max(...values); + const range = max - min || 1; // avoid divide by zero + + // Add padding to range so lines don't touch edges perfectly + const paddedMin = Math.max(0, min - (range * 0.1)); + const paddedMax = max + (range * 0.1); + const paddedRange = paddedMax - paddedMin; + + const width = 100; // viewBox width percent-ish + const points = data.map((d, i) => { + const x = (i / (data.length - 1)) * width; + const val = Number(d[dataKey]); + // Invert Y because SVG 0 is top + const y = 100 - ((val - paddedMin) / paddedRange) * 100; + return `${x},${y}`; + }).join(' '); + + return ( +
+
{label}
+ + {/* Grid lines (simple) */} + + + + + {/* The Data Line */} + + + {/* Interactive Dots (visible on hover) */} + {data.map((d, i) => { + const x = (i / (data.length - 1)) * width; + const val = Number(d[dataKey]); + const y = 100 - ((val - paddedMin) / paddedRange) * 100; + return ( + + {`${d.date.split('T')[0]}: ${val}`} + + ); + })} + +
+ {data[0]?.date.split('T')[0]} + {data[data.length - 1]?.date.split('T')[0]} +
+
+ ); +}; diff --git a/plugins/web/src/components/Sidebar.tsx b/plugins/web/src/components/Sidebar.tsx index afff422..80baa82 100644 --- a/plugins/web/src/components/Sidebar.tsx +++ b/plugins/web/src/components/Sidebar.tsx @@ -4,9 +4,11 @@ import { LayoutDashboard, Network, FileText, Settings, Layers, ChevronRight, X } interface SidebarProps { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; + currentView: string; + setCurrentView: (view: string) => void; } -export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => { +export const Sidebar = ({ isOpen, setIsOpen, currentView, setCurrentView }: SidebarProps) => { return ( <> {/* Mobile Overlay */} @@ -42,14 +44,39 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => { {/* Navigation */} @@ -69,8 +96,10 @@ const SidebarGroup = ({ title, children }: { title: string, children: React.Reac
); -const SidebarItem = ({ icon: Icon, label, active }: any) => ( -