forked from 0xWheatyz/SPARC
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97048917f2 | |||
| 88abd9574b | |||
| e0ed39908e | |||
| 87e09b365b |
+2
-1
@@ -49,7 +49,7 @@ services:
|
|||||||
init-db:
|
init-db:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
volumes:
|
volumes:
|
||||||
- ./patents:/app/patents
|
- patent_data:/app/patents
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Optional: MinIO for S3-compatible local object storage
|
# Optional: MinIO for S3-compatible local object storage
|
||||||
@@ -86,4 +86,5 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
patent_data:
|
||||||
minio_data:
|
minio_data:
|
||||||
|
|||||||
+76
-1
@@ -276,7 +276,7 @@ The `docker-compose.yml` includes all services needed for production:
|
|||||||
|---------|-----------|------|-------------|
|
|---------|-----------|------|-------------|
|
||||||
| `postgres` | sparc-postgres | 5432 | PostgreSQL database |
|
| `postgres` | sparc-postgres | 5432 | PostgreSQL database |
|
||||||
| `init-db` | sparc-init-db | - | One-time database initialization (seeds admin user) |
|
| `init-db` | sparc-init-db | - | One-time database initialization (seeds admin user) |
|
||||||
| `api` | sparc-api | 8000 | FastAPI REST API with JWT auth |
|
| `api` | sparc-api | 8000 | FastAPI REST API with JWT auth (patent PDFs stored in `patent_data` volume) |
|
||||||
| `dashboard` | sparc-dashboard | 8080 | React TypeScript web UI |
|
| `dashboard` | sparc-dashboard | 8080 | React TypeScript web UI |
|
||||||
|
|
||||||
### Common Docker Compose Commands
|
### Common Docker Compose Commands
|
||||||
@@ -307,6 +307,81 @@ docker-compose restart api
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Patent PDF Storage
|
||||||
|
|
||||||
|
The SPARC API downloads patent PDFs during analysis and stores them at `/app/patents` inside the container. These files are used for subsequent single-patent analysis requests and as a local cache to avoid re-downloading. If this directory is not persisted, all downloaded PDFs are lost when the container is recreated.
|
||||||
|
|
||||||
|
### Docker Compose (default)
|
||||||
|
|
||||||
|
The default `docker-compose.yml` declares a named volume called `patent_data` that is mounted at `/app/patents`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# In the api service:
|
||||||
|
volumes:
|
||||||
|
- patent_data:/app/patents
|
||||||
|
|
||||||
|
# At the top-level volumes section:
|
||||||
|
volumes:
|
||||||
|
patent_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
This means PDFs survive `docker compose down` and `docker compose up` cycles. To remove patent data intentionally, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down -v # WARNING: also removes postgres_data
|
||||||
|
# or selectively:
|
||||||
|
docker volume rm sparc_patent_data
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer a bind mount (e.g., for easy host-side access during development), replace the volume with:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./patents:/app/patents
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
For Kubernetes deployments, create a PersistentVolumeClaim and mount it into the API pod:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: sparc-patent-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: sparc-api
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: api
|
||||||
|
volumeMounts:
|
||||||
|
- name: patent-data
|
||||||
|
mountPath: /app/patents
|
||||||
|
volumes:
|
||||||
|
- name: patent-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: sparc-patent-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust the storage size based on expected patent volume. Each patent PDF is typically 1-5 MB.
|
||||||
|
|
||||||
|
### S3 Object Storage (alternative)
|
||||||
|
|
||||||
|
For production deployments that need shared or highly durable storage, set `STORAGE_BACKEND=s3` in your `.env` file. This stores patent PDFs in an S3-compatible bucket (AWS S3 or MinIO) instead of the local filesystem, eliminating the need for a persistent volume. See the S3/MinIO section in `.env.example` for configuration details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Database Connection Issues
|
### Database Connection Issues
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useTheme } from './ThemeContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns theme-aware color values for recharts components.
|
||||||
|
*
|
||||||
|
* Recharts accepts only raw color strings (not CSS variables),
|
||||||
|
* so this hook bridges the Tailwind/CSS-variable theme system
|
||||||
|
* to the imperative recharts API.
|
||||||
|
*/
|
||||||
|
export function useChartTheme() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Axis tick and grid line stroke color */
|
||||||
|
axisStroke: isDark ? '#94a3b8' : '#64748b',
|
||||||
|
/** Tooltip container background */
|
||||||
|
tooltipBg: isDark ? '#1e293b' : '#ffffff',
|
||||||
|
/** Tooltip container border */
|
||||||
|
tooltipBorder: isDark
|
||||||
|
? '1px solid rgba(99, 102, 241, 0.3)'
|
||||||
|
: '1px solid rgba(99, 102, 241, 0.2)',
|
||||||
|
/** Tooltip label text color */
|
||||||
|
tooltipLabelColor: isDark ? '#f8fafc' : '#0f172a',
|
||||||
|
/** Tooltip item text color */
|
||||||
|
tooltipItemColor: isDark ? '#e2e8f0' : '#334155',
|
||||||
|
/** Convenience: full contentStyle object for recharts Tooltip */
|
||||||
|
tooltipContentStyle: {
|
||||||
|
backgroundColor: isDark ? '#1e293b' : '#ffffff',
|
||||||
|
border: isDark
|
||||||
|
? '1px solid rgba(99, 102, 241, 0.3)'
|
||||||
|
: '1px solid rgba(99, 102, 241, 0.2)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: isDark ? '#f8fafc' : '#0f172a',
|
||||||
|
},
|
||||||
|
/** Convenience: labelStyle for recharts Tooltip */
|
||||||
|
tooltipLabelStyle: {
|
||||||
|
color: isDark ? '#f8fafc' : '#0f172a',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { analyticsApi } from '../api/client';
|
import { analyticsApi } from '../api/client';
|
||||||
import { AlertCircle, Database } from 'lucide-react';
|
import { AlertCircle, Database } from 'lucide-react';
|
||||||
import { PieChart, Pie, Cell, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
import { PieChart, Pie, Cell, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||||
|
import { useChartTheme } from '../context/useChartTheme';
|
||||||
|
|
||||||
const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
|
const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
|
||||||
|
|
||||||
export function AnalyticsPage() {
|
export function AnalyticsPage() {
|
||||||
const [days, setDays] = useState(30);
|
const [days, setDays] = useState(30);
|
||||||
|
const chartTheme = useChartTheme();
|
||||||
|
|
||||||
const { data, isLoading, isError, refetch } = useQuery({
|
const { data, isLoading, isError, refetch } = useQuery({
|
||||||
queryKey: ['analytics', days],
|
queryKey: ['analytics', days],
|
||||||
@@ -160,11 +162,7 @@ export function AnalyticsPage() {
|
|||||||
))}
|
))}
|
||||||
</Pie>
|
</Pie>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
</PieChart>
|
</PieChart>
|
||||||
@@ -178,15 +176,11 @@ export function AnalyticsPage() {
|
|||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">Analysis Types</h3>
|
<h3 className="text-lg font-semibold text-text-primary mb-4">Analysis Types</h3>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<BarChart data={typeData}>
|
<BarChart data={typeData}>
|
||||||
<XAxis dataKey="name" stroke="#94a3b8" fontSize={12} />
|
<XAxis dataKey="name" stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<YAxis stroke="#94a3b8" fontSize={12} />
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
labelStyle={chartTheme.tooltipLabelStyle}
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: '#f8fafc' }}
|
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="count" fill="#6366f1" radius={[4, 4, 0, 0]} />
|
<Bar dataKey="count" fill="#6366f1" radius={[4, 4, 0, 0]} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
@@ -222,15 +216,11 @@ export function AnalyticsPage() {
|
|||||||
<h4 className="text-md font-semibold text-text-primary mb-4">Analyses per Company Over Time</h4>
|
<h4 className="text-md font-semibold text-text-primary mb-4">Analyses per Company Over Time</h4>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<LineChart data={pivoted}>
|
<LineChart data={pivoted}>
|
||||||
<XAxis dataKey="month" stroke="#94a3b8" fontSize={12} />
|
<XAxis dataKey="month" stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<YAxis stroke="#94a3b8" fontSize={12} />
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
labelStyle={chartTheme.tooltipLabelStyle}
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: '#f8fafc' }}
|
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
{companies.map((company, idx) => (
|
{companies.map((company, idx) => (
|
||||||
@@ -268,15 +258,11 @@ export function AnalyticsPage() {
|
|||||||
<h4 className="text-md font-semibold text-text-primary mb-4">Analysis Types Over Time</h4>
|
<h4 className="text-md font-semibold text-text-primary mb-4">Analysis Types Over Time</h4>
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<BarChart data={pivoted}>
|
<BarChart data={pivoted}>
|
||||||
<XAxis dataKey="month" stroke="#94a3b8" fontSize={12} />
|
<XAxis dataKey="month" stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<YAxis stroke="#94a3b8" fontSize={12} />
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
labelStyle={chartTheme.tooltipLabelStyle}
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: '#f8fafc' }}
|
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
{types.map((type, idx) => (
|
{types.map((type, idx) => (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
|||||||
import { analysisApi } from '../api/client';
|
import { analysisApi } from '../api/client';
|
||||||
import { Rocket, CheckCircle, AlertCircle, ChevronDown, ChevronUp, RefreshCw, Inbox } from 'lucide-react';
|
import { Rocket, CheckCircle, AlertCircle, ChevronDown, ChevronUp, RefreshCw, Inbox } from 'lucide-react';
|
||||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
|
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
|
||||||
|
import { useChartTheme } from '../context/useChartTheme';
|
||||||
import type { BatchAnalysisResult } from '../types';
|
import type { BatchAnalysisResult } from '../types';
|
||||||
|
|
||||||
export function Batch() {
|
export function Batch() {
|
||||||
@@ -12,6 +13,8 @@ export function Batch() {
|
|||||||
const [result, setResult] = useState<BatchAnalysisResult | null>(null);
|
const [result, setResult] = useState<BatchAnalysisResult | null>(null);
|
||||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const chartTheme = useChartTheme();
|
||||||
|
|
||||||
const modelsQuery = useQuery({
|
const modelsQuery = useQuery({
|
||||||
queryKey: ['models'],
|
queryKey: ['models'],
|
||||||
queryFn: () => analysisApi.listModels(),
|
queryFn: () => analysisApi.listModels(),
|
||||||
@@ -210,15 +213,11 @@ export function Batch() {
|
|||||||
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
|
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<BarChart data={chartData}>
|
<BarChart data={chartData}>
|
||||||
<XAxis dataKey="name" stroke="#94a3b8" fontSize={12} />
|
<XAxis dataKey="name" stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<YAxis stroke="#94a3b8" fontSize={12} />
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={chartTheme.tooltipContentStyle}
|
||||||
backgroundColor: '#1e293b',
|
labelStyle={chartTheme.tooltipLabelStyle}
|
||||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: '#f8fafc' }}
|
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="patents" radius={[4, 4, 0, 0]}>
|
<Bar dataKey="patents" radius={[4, 4, 0, 0]}>
|
||||||
{chartData.map((entry, index) => (
|
{chartData.map((entry, index) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user