Docker Update and Fixes

This commit is contained in:
2026-01-25 17:36:15 +00:00
parent b7b718d4ca
commit 295be1ed8e
21 changed files with 886 additions and 289 deletions

View File

@@ -19,6 +19,8 @@
let useCamera = $state(true);
let fileInputElement = $state<HTMLInputElement>();
let alternatives = $state<any[]>([]);
onMount(() => {
const initCamera = async () => {
if (useCamera) {
@@ -43,6 +45,36 @@
};
});
async function analyzeImage(imageBase64: string) {
try {
const payload = {
product_name: "Scanned Item",
description: "Scanned via camera",
report_type: "product",
image: imageBase64,
user_id: localStorage.getItem("ethix_user_id") || "anonymous",
is_public: false,
};
const response = await fetch("/api/incidents/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await response.json();
if (data.status === "success" && data.analysis) {
return data;
} else {
throw new Error(data.message || "Analysis failed");
}
} catch (e) {
console.error("Analysis error:", e);
return null;
}
}
function playSuccessSound() {
const audio = new Audio("/report completed.mp3");
audio.volume = 0.5;
@@ -55,7 +87,7 @@
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
reader.onload = async (e) => {
const imageData = e.target?.result as string;
capturedImage = imageData;
analyzing = true;
@@ -73,7 +105,6 @@
analyzing = false;
showResult = true;
resultTranslateY = 0;
playSuccessSound();
typeText();
}, 1200);
};
@@ -88,25 +119,26 @@
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(videoElement, 0, 0);
capturedImage = canvas.toDataURL("image/png");
}
const imageData = canvas.toDataURL("image/png");
capturedImage = imageData;
setTimeout(() => {
const newItem = {
id: Date.now(),
title: "Plastic Bottle",
date: new Date().toLocaleString(),
impact: "High",
imageUri: capturedImage,
};
onScanComplete(newItem);
const result = await analyzeImage(imageData);
processResult(result, imageData);
} else {
analyzing = false;
}
}
function processResult(data: any, imageUri: string) {
analyzing = false;
if (!data) {
displayTitle = "Scan Failed";
alternatives = [];
showResult = true;
resultTranslateY = 0;
playSuccessSound();
typeText();
}, 1200);
}
@@ -164,7 +196,26 @@
<button
class="mode-toggle-btn"
onclick={() => (useCamera = !useCamera)}
onclick={() => {
useCamera = !useCamera;
if (useCamera) {
// Re-init camera if switching back
const initCamera = async () => {
try {
stream =
await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
});
if (videoElement) {
videoElement.srcObject = stream;
}
} catch (err) {
console.error("Camera access denied:", err);
}
};
initCamera();
}
}}
>
{#if useCamera}
<Icon
@@ -241,53 +292,36 @@
<h2 class="sheet-title">{displayTitle}</h2>
<p class="alternatives-label">Top Sustainable Alternatives</p>
<div class="alternatives-scroll">
<button class="alternative-card glass-bottle">
<div class="alt-header">
<Icon
icon="ri:cup-fill"
width="24"
style="color: #60a5fa;"
/>
<span class="rating">★ 4.9</span>
</div>
<h3 class="alt-name">Glass Bottle</h3>
<p class="alt-price">$2.49</p>
</button>
<button class="alternative-card boxed-water">
<div class="alt-header">
<Icon
icon="ri:box-3-fill"
width="24"
style="color: #a78bfa;"
/>
<span class="rating">★ 4.7</span>
</div>
<h3 class="alt-name">Boxed Water</h3>
<p class="alt-price">$1.89</p>
</button>
<button class="alternative-card aluminum">
<div class="alt-header">
<Icon
icon="ri:beer-fill"
width="24"
style="color: #9ca3af;"
/>
<span class="rating">★ 4.5</span>
</div>
<h3 class="alt-name">Aluminum</h3>
<p class="alt-price">$1.29</p>
</button>
</div>
<button class="report-btn" onclick={handleClose}>
<Icon icon="ri:alarm-warning-fill" width="20" />
<span>Report Greenwashing</span>
</button>
{#if alternatives && alternatives.length > 0}
<p class="alternatives-label">
Sustainable Alternatives
</p>
<div class="alternatives-scroll">
{#each alternatives as alt}
<div class="alternative-card">
<div class="alt-header">
<Icon
icon="ri:leaf-fill"
width="24"
style="color: #4ade80;"
/>
{#if alt.impact_reduction}
<span class="rating">Better</span>
{/if}
</div>
<p class="alt-name">{alt.name}</p>
</div>
{/each}
</div>
{:else if displayTitle !== "Scan Failed"}
<p class="alternatives-label" style="color: #4ade80;">
✓ No greenwashing concerns found
</p>
{:else}
<p class="alternatives-label" style="color: #f87171;">
Could not analyze this image. Please try again.
</p>
{/if}
</div>
{/if}
</div>
@@ -382,10 +416,11 @@
height: 80px;
border-radius: 50%;
background-color: white;
border: 6px solid rgba(255, 255, 255, 0.3);
border: 6px solid rgba(74, 222, 128, 0.5);
cursor: pointer;
transition: transform 0.2s;
background-clip: padding-box;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.shutter-button:active {

View File

@@ -14,15 +14,174 @@
];
interface ScanItem {
id: number;
id: string;
name: string;
date: string;
severity: string;
image: string;
image: string | null;
impact: string;
alternatives: string[];
is_greenwashing: boolean;
is_public: boolean;
}
let scanHistory = $state<ScanItem[]>([]);
let selectedScan = $state<ScanItem | null>(null);
let userId = $state("");
let fileInput: HTMLInputElement;
let isScanning = $state(false);
onMount(() => {
let storedId = localStorage.getItem("ethix_user_id");
if (!storedId) {
storedId = crypto.randomUUID();
localStorage.setItem("ethix_user_id", storedId);
}
userId = storedId;
fetchHistory();
// Listen for scan-complete event to refresh history
const handleScanComplete = () => {
fetchHistory();
};
window.addEventListener("scan-complete", handleScanComplete);
return () => {
window.removeEventListener("scan-complete", handleScanComplete);
};
});
async function fetchHistory() {
if (!userId) return;
try {
const res = await fetch(
`/api/incidents/history?user_id=${userId}`,
);
const data = await res.json();
if (Array.isArray(data)) {
scanHistory = data.map((item: any) => ({
id: item._id,
name: item.product_name,
date: new Date(item.created_at).toLocaleDateString(),
severity: item.analysis?.severity || "Low",
image: item.image_base64
? `data:image/jpeg;base64,${item.image_base64}`
: null,
impact: item.analysis?.verdict || "No impact assessment",
alternatives: item.analysis?.alternatives || [],
is_greenwashing: item.analysis?.is_greenwashing || false,
is_public: item.is_public || false,
}));
}
} catch (e) {
console.error("Failed to fetch history", e);
}
}
function triggerScan() {
if (isScanning) return;
fileInput.click();
}
async function handleFileSelect(e: Event) {
const target = e.target as HTMLInputElement;
if (!target.files || target.files.length === 0) return;
const file = target.files[0];
isScanning = true;
const reader = new FileReader();
reader.onload = async () => {
const base64String = reader.result as string;
await submitScan(base64String);
};
reader.readAsDataURL(file);
}
async function submitScan(imageBase64: string) {
try {
const payload = {
product_name: "Scanned Item",
description: "Scanned via mobile app",
report_type: "product",
image: imageBase64,
user_id: userId,
is_public: false, // Default private
};
const res = await fetch(
"/api/incidents/submit",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
},
);
const data = await res.json();
if (data.status === "success") {
await fetchHistory();
selectedScan = {
id: data.incident_id,
name:
data.detected_brand !== "Unknown"
? data.detected_brand
: "Scanned Product",
date: "Just now",
severity: data.analysis.severity,
image: imageBase64,
impact: data.analysis.verdict,
alternatives: data.analysis.alternatives || [],
is_greenwashing: data.is_greenwashing,
is_public: false,
};
} else {
alert("Analysis failed: " + (data.message || "Unknown error"));
}
} catch (e) {
console.error("Scan failed", e);
alert("Analysis failed. Please try again.");
} finally {
isScanning = false;
if (fileInput) fileInput.value = "";
}
}
async function toggleVisibility() {
if (!selectedScan) return;
const newState = !selectedScan.is_public;
// Optimistic UI update for modal
selectedScan.is_public = newState;
// Optimistic UI update for list
const histIndex = scanHistory.findIndex(
(s) => s.id === selectedScan!.id,
);
if (histIndex !== -1) {
scanHistory[histIndex].is_public = newState;
}
try {
await fetch(
`/api/incidents/${selectedScan.id}/visibility`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ is_public: newState }),
},
);
} catch (e) {
// Revert on failure
selectedScan.is_public = !newState;
if (histIndex !== -1) {
scanHistory[histIndex].is_public = !newState;
}
alert("Failed to update visibility");
}
}
function openScan(scan: ScanItem) {
selectedScan = scan;
@@ -32,69 +191,83 @@
selectedScan = null;
}
let greeting = $state("Hello");
onMount(() => {
const hour = new Date().getHours();
if (hour < 12) greeting = "Good morning";
else if (hour < 17) greeting = "Good afternoon";
else greeting = "Good evening";
});
function getSeverityClass(severity: string): string {
return severity.toLowerCase();
return severity?.toLowerCase() || "low";
}
</script>
<div class="min-h-screen bg-[#051f18] text-white px-5 pt-4 pb-32">
<section class="flex gap-3 mb-7">
<button
class="flex-1 flex flex-col items-center gap-2 py-4 px-2 bg-[#0d2e25] border border-emerald-500/30 rounded-2xl active:scale-95 transition-all shadow-[0_0_15px_rgba(16,185,129,0.1)] relative overflow-hidden group min-h-[100px] justify-center"
onclick={triggerScan}
disabled={isScanning}
>
<div
class="absolute inset-0 bg-emerald-500/5 group-hover:bg-emerald-500/10 transition-colors"
></div>
<div
class="w-10 h-10 bg-emerald-500/20 rounded-xl flex items-center justify-center text-emerald-400 relative z-10"
>
{#if isScanning}
<Icon
icon="ri:loader-4-line"
width="24"
class="animate-spin"
/>
{:else}
<Icon icon="ri:camera-lens-fill" width="24" />
{/if}
</div>
<div class="flex flex-col items-center relative z-10">
<span class="text-sm font-extrabold text-white"
>{isScanning ? "Analyzing..." : "Scan Item"}</span
>
<span
class="text-[10px] text-emerald-400/80 uppercase tracking-wide font-semibold"
>AI Detection</span
>
</div>
</button>
<div
class="flex-1 flex flex-col items-center gap-2 py-4 px-2 bg-[#0d2e25] border border-[#1f473b] rounded-2xl"
class="flex-1 flex flex-col items-center gap-2 py-4 px-2 bg-[#0d2e25] border border-[#1f473b] rounded-2xl min-h-[100px] justify-center"
>
<div
class="w-9 h-9 bg-emerald-500/10 rounded-xl flex items-center justify-center"
>
<Icon
icon="ri:qr-scan-2-line"
width="20"
class="text-emerald-400"
/>
<Icon icon="ri:leaf-fill" width="20" class="text-emerald-400" />
</div>
<span class="text-lg font-extrabold text-white">47</span>
<span class="text-lg font-extrabold text-white"
>{scanHistory.length}</span
>
<span
class="text-[10px] text-gray-400 uppercase tracking-wide font-semibold"
>scans</span
>
</div>
<div
class="flex-1 flex flex-col items-center gap-2 py-4 px-2 bg-[#0d2e25] border border-[#1f473b] rounded-2xl"
>
<div
class="w-9 h-9 bg-emerald-500/10 rounded-xl flex items-center justify-center"
>
<Icon
icon="ri:checkbox-circle-fill"
width="20"
class="text-emerald-400"
/>
</div>
<span class="text-lg font-extrabold text-white">32</span>
<span
class="text-[10px] text-gray-400 uppercase tracking-wide font-semibold"
>eco picks</span
>Total Scans</span
>
</div>
</section>
<section class="recent-section">
<div class="mb-4">
<h2 class="text-base font-extrabold text-white m-0">Your Scans</h2>
</div>
<div class="flex flex-col gap-2.5">
{#if scanHistory.length === 0}
<div class="text-center py-10 text-gray-500 text-sm">
No scans yet. Start by scanning a product!
</div>
{/if}
{#each scanHistory as item (item.id)}
<button
class="flex items-center gap-3.5 p-5 bg-[#0d2e25] border border-[#1f473b] rounded-2xl cursor-pointer w-full text-left transition-transform active:scale-98 active:bg-[#1f473b]"
onclick={() => openScan(item)}
aria-label="View {item.name}"
>
<div class="w-12 h-12 rounded-xl overflow-hidden shrink-0">
<div
class="w-12 h-12 rounded-xl overflow-hidden shrink-0 bg-black"
>
{#if item.image}
<img
src={item.image}
@@ -110,7 +283,9 @@
? '#fca5a5'
: '#86efac'}"
>
<span class="text-lg">{item.name[0]}</span>
<span class="text-lg"
>{item.name ? item.name[0] : "?"}</span
>
</div>
{/if}
</div>
@@ -118,7 +293,17 @@
<span class="text-sm font-semibold text-white truncate"
>{item.name}</span
>
<span class="text-xs text-gray-400">{item.date}</span>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-400"
>{item.date}</span
>
{#if item.is_public}
<span
class="text-[9px] px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-400 font-bold border border-emerald-500/30"
>PUBLIC</span
>
{/if}
</div>
</div>
<span
class="px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wide {item.severity.toLowerCase() ===
@@ -137,19 +322,17 @@
{#if selectedScan}
<div
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-5"
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[200] flex items-start justify-center p-4 pt-8 pb-4 overflow-y-auto"
role="button"
tabindex="0"
onkeydown={(e) => e.key === "Escape" && closeScan()}
onclick={closeScan}
onkeydown={(e) => e.key === "Escape" && closeScan()}
>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="bg-[#0d2e25] border border-[#1f473b] rounded-3xl w-full max-w-[340px] p-6 relative shadow-[0_20px_50px_rgba(0,0,0,0.5)]"
class="bg-[#0d2e25] border border-[#1f473b] rounded-3xl w-full max-w-[340px] p-5 relative shadow-[0_20px_50px_rgba(0,0,0,0.5)] mb-4"
onclick={(e) => e.stopPropagation()}
role="document"
tabindex="-1"
onkeydown={(e) => e.key === "Escape" && closeScan()}
onclick={(e) => e.stopPropagation()}
>
<button
class="absolute top-4 right-4 bg-transparent border-none text-gray-400 cursor-pointer"
@@ -157,7 +340,7 @@
>
<Icon icon="ri:close-fill" width="24" />
</button>
<div class="text-center mb-5">
<div class="text-center mb-4">
<span
class="text-xs text-gray-400 uppercase tracking-widest"
>{selectedScan.date}</span
@@ -165,7 +348,7 @@
<h2 class="text-xl text-white m-0 mt-1">Scan Report</h2>
</div>
<div
class="w-full h-[200px] bg-black rounded-2xl mb-5 overflow-hidden"
class="w-full h-[160px] bg-black rounded-2xl mb-4 overflow-hidden"
>
{#if selectedScan.image}
<img
@@ -183,7 +366,9 @@
: '#86efac'}"
>
<span class="text-6xl text-[#051f18] font-black"
>{selectedScan.name[0]}</span
>{selectedScan.name
? selectedScan.name[0]
: "?"}</span
>
</div>
{/if}
@@ -192,19 +377,64 @@
<h3 class="text-lg text-white m-0 mb-1">
{selectedScan.name}
</h3>
<p class="text-gray-300 text-sm mb-3">
<div class="flex justify-center gap-2 my-4">
<div
class="inline-block px-3 py-1.5 rounded-full text-xs font-bold uppercase {selectedScan.severity.toLowerCase() ===
'high'
? 'bg-red-400/20 text-red-400'
: selectedScan.severity.toLowerCase() ===
'medium'
? 'bg-orange-400/20 text-orange-400'
: 'bg-emerald-400/20 text-emerald-400'}"
>
Severity: {selectedScan.severity}
</div>
</div>
<!-- Visibility Toggle -->
<button
class="mb-4 flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-xs font-bold w-full border {selectedScan.is_public
? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/50'
: 'bg-[#051f18] text-gray-400 border-[#1f473b]'}"
onclick={toggleVisibility}
>
<div
class="w-3 h-3 rounded-full {selectedScan.is_public
? 'bg-emerald-400'
: 'bg-gray-500'}"
></div>
{selectedScan.is_public
? "PUBLIC REPORT (Visible to everyone)"
: "PRIVATE REPORT (Only you)"}
</button>
<p class="text-gray-300 text-sm mb-4 leading-relaxed">
{selectedScan.impact}
</p>
<div
class="inline-block px-3 py-1.5 rounded-full text-xs font-bold uppercase {selectedScan.severity.toLowerCase() ===
'high'
? 'bg-red-400/20 text-red-400'
: selectedScan.severity.toLowerCase() === 'medium'
? 'bg-orange-400/20 text-orange-400'
: 'bg-emerald-400/20 text-emerald-400'}"
>
Severity: {selectedScan.severity}
</div>
{#if selectedScan.alternatives && selectedScan.alternatives.length > 0}
<div
class="mt-6 text-left bg-emerald-900/20 p-4 rounded-xl border border-emerald-500/20 mb-2"
>
<h4
class="text-emerald-400 text-xs font-bold uppercase tracking-widest mb-3 flex items-center gap-2"
>
<Icon icon="ri:leaf-line" />
Sustainable Alternatives
</h4>
<ul class="text-sm text-gray-300 space-y-2 pl-1">
{#each selectedScan.alternatives as alt}
<li class="flex items-start gap-2">
<span class="text-emerald-500 mt-1"
></span
>
<span>{alt}</span>
</li>
{/each}
</ul>
</div>
{/if}
</div>
</div>
</div>

View File

@@ -17,8 +17,8 @@
const PATH_CONFIG: Record<string, SceneConfig> = {
"/": { sceneType: "transition", staticScene: false },
"/chat": { sceneType: "oilRig", staticScene: true },
"/community": { sceneType: "forest", staticScene: true },
"/report": { sceneType: "industrial", staticScene: true },
"/goal": { sceneType: "pollutedCity", staticScene: true },
"/report": { sceneType: "deforestation", staticScene: true },
"/catalogue": {
sceneType: "transition",
staticScene: false,

View File

@@ -18,7 +18,7 @@
async function fetchStats() {
try {
const res = await fetch("http://localhost:5000/api/reports/stats");
const res = await fetch("/api/reports/stats");
const data = await res.json();
statsData = data;
} catch (e) {

View File

@@ -59,7 +59,7 @@
.toLowerCase()
.endsWith(".txt")}
<iframe
src="http://localhost:5000/api/reports/view/{report.filename}"
src="/api/reports/view/{report.filename}"
class="w-full h-full border-none"
title="Report Viewer"
></iframe>
@@ -74,7 +74,7 @@
/>
<p>Preview not available for this file type.</p>
<a
href="http://localhost:5000/api/reports/view/{report.filename}"
href="/api/reports/view/{report.filename}"
download
class="bg-emerald-400 text-slate-900 px-6 py-3 rounded-full no-underline font-bold transition-transform hover:scale-105"
>

View File

@@ -0,0 +1,52 @@
export const API_BASE = '/api';
export function apiUrl(path: string): string {
// Ensure path starts with /
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return `${API_BASE}${normalizedPath}`;
}
export async function apiFetch<T = any>(
path: string,
options: RequestInit = {}
): Promise<T> {
const url = apiUrl(path);
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`);
}
return response.json();
}
export async function apiGet<T = any>(path: string): Promise<T> {
return apiFetch<T>(path, { method: 'GET' });
}
export async function apiPost<T = any>(path: string, body: any): Promise<T> {
return apiFetch<T>(path, {
method: 'POST',
body: JSON.stringify(body),
});
}
export async function apiPut<T = any>(path: string, body: any): Promise<T> {
return apiFetch<T>(path, {
method: 'PUT',
body: JSON.stringify(body),
});
}
export function reportViewUrl(filename: string): string {
return `${API_BASE}/reports/view/${encodeURIComponent(filename)}`;
}

View File

@@ -22,8 +22,9 @@ const SCENE_ELEMENTS: Record<Exclude<SceneType, 'transition'>, (dc: DrawContext)
pollutedCity: drawPollutedCityScene,
};
const CUSTOM_WATER_SCENES: SceneType[] = ['ocean', 'oilRig'];
const NO_TERRAIN_SCENES: SceneType[] = ['ocean', 'oilRig'];
const CUSTOM_WATER_SCENES: SceneType[] = ['ocean', 'oilRig', 'deforestation'];
const NO_MOUNTAINS_SCENES: SceneType[] = ['ocean', 'oilRig', 'deforestation'];
const NO_HILLS_SCENES: SceneType[] = ['ocean', 'oilRig'];
export function drawLandscape(
ctx: CanvasRenderingContext2D,
@@ -40,11 +41,16 @@ export function drawLandscape(
drawSun(dc);
drawClouds(dc);
const skipTerrain = NO_TERRAIN_SCENES.includes(sceneType) ||
(blendToScene && NO_TERRAIN_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5);
const skipMountains = NO_MOUNTAINS_SCENES.includes(sceneType) ||
(blendToScene && NO_MOUNTAINS_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5);
if (!skipTerrain) {
const skipHills = NO_HILLS_SCENES.includes(sceneType) ||
(blendToScene && NO_HILLS_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5);
if (!skipMountains) {
drawMountains(dc);
}
if (!skipHills) {
drawHills(dc);
}

View File

@@ -19,8 +19,10 @@ export function drawSmoggyBuildings(dc: DrawContext): void {
for (let row = 0; row < windowRows; row++) {
for (let col = 0; col < windowCols; col++) {
const isBroken = Math.random() > 0.85;
const isLit = Math.random() > 0.5;
// Deterministic "randomness" based on position to stop flashing
const seed = x + col * 13 + row * 71;
const isBroken = Math.abs(Math.sin(seed)) > 0.85;
const isLit = Math.cos(seed) > 0.1;
if (!isBroken) {
ctx.fillStyle = isLit ? 'rgba(255, 180, 100, 0.5)' : 'rgba(50, 50, 50, 0.8)';

View File

@@ -32,7 +32,14 @@
function handleScanComplete(item: any) {
recentItems = [item, ...recentItems];
// Don't close camera here - let user view the result sheet first
// Camera will close when user clicks close button
}
function handleCameraClose() {
isCameraActive = false;
// Dispatch event to notify MobileHomePage to refresh
window.dispatchEvent(new CustomEvent('scan-complete'));
}
onMount(() => {
@@ -147,7 +154,7 @@
{#if isCameraActive}
<CameraScreen
onClose={() => (isCameraActive = false)}
onClose={handleCameraClose}
onScanComplete={handleScanComplete}
/>
{/if}

View File

@@ -66,7 +66,7 @@
async function fetchReports() {
isLoading = true;
try {
const res = await fetch("http://localhost:5000/api/reports/");
const res = await fetch("/api/reports/");
const data = await res.json();
if (Array.isArray(data)) reports = data;
} catch (e) {
@@ -79,7 +79,7 @@
async function fetchIncidents() {
isLoading = true;
try {
const res = await fetch("http://localhost:5000/api/incidents/list");
const res = await fetch("/api/incidents/list");
const data = await res.json();
if (Array.isArray(data)) incidents = data;
} catch (e) {
@@ -103,7 +103,7 @@
isLoading = true;
try {
const res = await fetch(
"http://localhost:5000/api/reports/search",
"/api/reports/search",
{
method: "POST",
headers: { "Content-Type": "application/json" },

View File

@@ -52,7 +52,7 @@
try {
const response = await fetch(
"http://localhost:5000/api/gemini/ask",
"/api/gemini/ask",
{
method: "POST",
headers: { "Content-Type": "application/json" },

View File

@@ -130,7 +130,7 @@
if (reportType === "company") {
// Use the new upload endpoint for company reports
const response = await fetch(
"http://localhost:5000/api/reports/upload",
"/api/reports/upload",
{
method: "POST",
headers: {
@@ -170,7 +170,7 @@
} else {
// Original product incident flow
const response = await fetch(
"http://localhost:5000/api/incidents/submit",
"/api/incidents/submit",
{
method: "POST",
headers: {