Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion TriNetra/backend/api/chronos_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
fetch_transactions_since,
fetch_all_transactions,
search_transactions as db_search_transactions,
append_transactions,
)

chronos_bp = Blueprint('chronos', __name__)
Expand Down Expand Up @@ -154,7 +155,100 @@ def search_transactions():
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500

# Helper Functions
@chronos_bp.route('/import-csv', methods=['POST'])
def import_csv():
"""Import transactions from an uploaded CSV file into the database."""
try:
if 'file' not in request.files:
return jsonify({'status': 'error', 'message': 'No file provided'}), 400

file = request.files['file']
if not file or file.filename == '':
return jsonify({'status': 'error', 'message': 'No file selected'}), 400
if not file.filename.lower().endswith('.csv'):
return jsonify({'status': 'error', 'message': 'Only CSV files are accepted'}), 400

import io
raw = file.read()
# Attempt UTF-8 then fall back to latin-1 for wider compatibility
try:
content = raw.decode('utf-8')
except UnicodeDecodeError:
try:
content = raw.decode('latin-1')
except Exception:
return jsonify({
'status': 'error',
'message': 'File encoding not supported. Please save the CSV as UTF-8.'
}), 400
df = pd.read_csv(io.StringIO(content))

# Validate required columns
required_cols = ['transaction_id', 'from_account', 'to_account', 'amount']
missing = [c for c in required_cols if c not in df.columns]
if missing:
return jsonify({
'status': 'error',
'message': f'Missing required columns: {", ".join(missing)}'
}), 400

now = datetime.now().isoformat()

# Fill optional columns with sensible defaults
if 'timestamp' not in df.columns:
df['timestamp'] = now
else:
df['timestamp'] = df['timestamp'].fillna(now)

if 'suspicious_score' not in df.columns:
df['suspicious_score'] = 0.0
else:
df['suspicious_score'] = pd.to_numeric(df['suspicious_score'], errors='coerce').fillna(0.0)

# Validate that 'amount' column contains numeric values
df['amount'] = pd.to_numeric(df['amount'], errors='coerce')
invalid_amounts = df['amount'].isna().sum()
if invalid_amounts > 0:
return jsonify({
'status': 'error',
'message': f'{invalid_amounts} row(s) have invalid non-numeric values in the "amount" column.'
}), 400

if 'pattern_type' not in df.columns:
df['pattern_type'] = 'imported'
else:
df['pattern_type'] = df['pattern_type'].fillna('imported')

if 'scenario' not in df.columns:
df['scenario'] = 'imported'
else:
df['scenario'] = df['scenario'].fillna('imported')

if 'transaction_type' not in df.columns:
df['transaction_type'] = 'IMPORT'
else:
df['transaction_type'] = df['transaction_type'].fillna('IMPORT')

# Keep only the columns the database expects
keep_cols = ['transaction_id', 'from_account', 'to_account', 'amount',
'timestamp', 'suspicious_score', 'pattern_type', 'scenario',
'transaction_type']
records = df[keep_cols].to_dict('records')

inserted = append_transactions(records)

return jsonify({
'status': 'success',
'message': f'{inserted} transaction(s) imported successfully '
f'({len(records) - inserted} duplicate(s) skipped)',
'imported_count': inserted,
'total_rows': len(records)
})

except Exception as e:
import logging
logging.getLogger(__name__).error('CSV import error: %s', e, exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred while processing the file.'}), 500

def generate_aadhar_location():
"""Generate realistic Aadhar-based location data"""
Expand Down
54 changes: 53 additions & 1 deletion TriNetra/backend/database/db_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def count_transactions() -> int:


def bulk_insert_transactions(records: list[dict]) -> None:
"""Insert a list of transaction dicts."""
"""Insert a list of transaction dicts (replaces table – used for initial seeding)."""
if not records:
return
sb = _supabase()
Expand All @@ -226,6 +226,58 @@ def bulk_insert_transactions(records: list[dict]) -> None:
conn.close()


def append_transactions(records: list[dict]) -> int:
"""Append new transaction records without replacing existing data.

Duplicate ``transaction_id`` values are silently skipped.
Returns the number of rows actually inserted.
"""
if not records:
return 0
sb = _supabase()
if sb is not None:
chunk_size = 500
inserted = 0
for i in range(0, len(records), chunk_size):
chunk = records[i:i + chunk_size]
res = sb.table('transactions').upsert(chunk, on_conflict='transaction_id').execute()
inserted += len(res.data) if res.data else 0
return inserted
# SQLite fallback
cfg = _get_config()
conn = sqlite3.connect(cfg.DATABASE_PATH)
cursor = conn.cursor()
inserted = 0
failed = 0
for rec in records:
try:
cursor.execute(
"INSERT OR IGNORE INTO transactions "
"(transaction_id, from_account, to_account, amount, timestamp, "
" transaction_type, suspicious_score, pattern_type, scenario) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
rec.get('transaction_id'),
rec.get('from_account'),
rec.get('to_account'),
rec.get('amount'),
rec.get('timestamp'),
rec.get('transaction_type'),
rec.get('suspicious_score', 0.0),
rec.get('pattern_type'),
rec.get('scenario'),
),
)
inserted += cursor.rowcount
except Exception as exc:
failed += 1
import logging
logging.getLogger(__name__).warning('Failed to insert record %s: %s', rec.get('transaction_id'), exc)
conn.commit()
conn.close()
return inserted


# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
Expand Down
43 changes: 43 additions & 0 deletions TriNetra/frontend-react/src/pages/ChronosPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import { TimelineIcon } from '../components/Icons'
export default function ChronosPage() {
const navigate = useNavigate()
const timelineRef = useRef(null)
const csvInputRef = useRef(null)

const [speed, setSpeed] = useState(1)
const [viewMode, setViewMode] = useState('timeline')
const [networkFilter, setNetworkFilter] = useState('all')
const [insights, setInsights] = useState([])
const [insightsLoading, setInsightsLoading] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [importing, setImporting] = useState(false)

/* ── Handlers ── */
const handleTimeQuantumChange = async (e) => {
Expand Down Expand Up @@ -138,6 +140,28 @@ export default function ChronosPage() {
timelineRef.current?.exportReport()
}

const handleCSVImport = async (e) => {
const file = e.target.files?.[0]
if (!file) return
// Reset input so the same file can be re-selected
e.target.value = ''

setImporting(true)
try {
const api = (await import('../services/api.js')).default
const result = await api.importCSV(file)
if (result.status === 'success') {
notify(result.message, 'success')
} else {
notify(result.message || 'Import failed', 'error')
}
} catch (err) {
notify('CSV import failed: ' + err.message, 'error')
} finally {
setImporting(false)
}
}

return (
<div className="text-white">
<Navbar pageTitle="CHRONOS" pageIcon="🕐" pageTitleColor="text-[#00ff87]" />
Expand Down Expand Up @@ -237,6 +261,25 @@ export default function ChronosPage() {
Export Report
</button>
</div>

{/* Import CSV */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-300">Import Data</label>
<input
ref={csvInputRef}
type="file"
accept=".csv"
className="hidden"
onChange={handleCSVImport}
/>
<button
onClick={() => csvInputRef.current?.click()}
disabled={importing}
className="w-full px-4 py-2 bg-[#7C3AED] hover:bg-[#7C3AED]/80 text-white rounded-lg transition-all uppercase text-sm font-semibold tracking-wide disabled:opacity-50"
>
{importing ? 'Importing…' : '📥 Import CSV'}
</button>
</div>
</div>
</div>

Expand Down
16 changes: 16 additions & 0 deletions TriNetra/frontend-react/src/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,22 @@ Generated by TriNetra Auto-SAR System (Demo Mode)
};
}

async importCSV(file) {
const formData = new FormData()
formData.append('file', file)
const url = `${this.baseURL}/chronos/import-csv`
try {
const response = await fetch(url, { method: 'POST', body: formData })
const contentType = response.headers.get('content-type')
if (!contentType || !contentType.includes('application/json')) {
return { status: 'error', message: 'Server returned a non-JSON response' }
}
return await response.json()
} catch (error) {
return { status: 'error', message: error.message }
}
}

// CHRONOS API calls
async getTimelineData(scenario = 'all', timeQuantum = '1m') {
return this.request(`/chronos/timeline?scenario=${scenario}&time_quantum=${timeQuantum}`);
Expand Down