Skip to content
Merged
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
1 change: 1 addition & 0 deletions FreshScanAi
Submodule FreshScanAi added at af3b16
19 changes: 19 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from contextlib import asynccontextmanager
from typing import Optional


# Load .env file if present (python-dotenv)
try:
from dotenv import load_dotenv
Expand Down Expand Up @@ -91,6 +92,7 @@ async def lifespan(app: FastAPI):
)
yield

from fastapi import FastAPI

app = FastAPI(title="FreshScan AI", version="1.1.0", lifespan=lifespan)

Expand Down Expand Up @@ -346,6 +348,23 @@ async def get_me(current_user=Depends(get_current_user)):
}


@app.get("/api/v1/public/report/{scan_id}")
async def get_public_report(scan_id: str):
try:
resp = _db().table("scans").select("*").eq("id", scan_id).execute()
if not resp.data:
raise HTTPException(status_code=404, detail="Scan not found")
return {"success": True, "scan": resp.data[0]}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))

# ── MAIN ──────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)


# ── SCAN ──────────────────────────────────────────────────────────────────────

@app.post("/api/v1/scan")
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MarketMapPage from './pages/MarketMapPage';
import ResultsPage from './pages/ResultsPage';
import PostHogPageView from './components/PostHogPageView';
import NotFound from './pages/NotFound';
import PublicReport from "./pages/PublicReport";

export default function App() {
return (
Expand All @@ -32,6 +33,7 @@ export default function App() {

{/* Catch-all route for broken links/404s */}
<Route path="*" element={<NotFound />} />
<Route path="/report/:id" element={<PublicReport />} />
</Route>
</Routes>
</BrowserRouter>
Expand Down
4 changes: 4 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,8 @@ input::placeholder {
--color-outline: #6b6b6b;
--color-outline-variant: #c9c9c9;
--color-tertiary: #121212;
}

@media print {
nav, .print\:hidden { display: none !important; }
}
107 changes: 107 additions & 0 deletions src/pages/PublicReport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import StatusTerminal from '../components/StatusTerminal';

interface ScanData {
id: string;
created_at: string;
freshness_score: number;
grade: string;
label: string;
markers: Record<string, unknown>;
}

export default function PublicReport() {
const { id } = useParams();
const [scan, setScan] = useState<ScanData | null>(null);
const [copied, setCopied] = useState(false);
const [error, setError] = useState(false);

useEffect(() => {
fetch(`/api/v1/public/report/${id}`)
.then(r => {
if (!r.ok) throw new Error("Not found");
return r.json();
})
.then((data) => setScan(data.scan))
.catch(() => setError(true));
}, [id]);

const handleShare = () => {
navigator.clipboard.writeText(window.location.href);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

const handlePrint = () => window.print();

if (error) {
return (
<div className="min-h-screen bg-surface-lowest flex items-center justify-center p-6">
<StatusTerminal messages={['ERROR: 404', 'REPORT_NOT_FOUND', 'VERIFY_SCAN_ID']} className="max-w-md w-full" />
</div>
);
}

if (!scan) {
return (
<div className="min-h-screen bg-surface-lowest flex items-center justify-center p-6">
<StatusTerminal messages={['FETCHING_DATA', 'STANDBY...']} className="max-w-md w-full" />
</div>
);
}

return (
<div className="min-h-screen bg-surface-lowest p-6 md:p-12 print:p-0 print:bg-white flex flex-col">
<div className="max-w-3xl mx-auto w-full flex-1">

{/* Header */}
<div className="border-b border-outline-variant/30 pb-6 mb-8 print:border-black">
<h1 className="font-display text-2xl md:text-3xl font-bold tracking-tight uppercase">
FreshScan <span className="text-neon print:text-black">Public Report</span>
</h1>
<p className="font-mono text-[0.65rem] tracking-widest text-on-surface-variant mt-2 uppercase">
GENERATED: {new Date(scan.created_at).toLocaleString()} | ID: {scan.id}
</p>
</div>

{/* Score Card */}
<div className="bg-surface-low border border-outline-variant/30 p-8 mb-8 print:border-black print:bg-white text-center">
<p className="font-mono text-[0.65rem] tracking-widest text-on-surface-variant mb-2">FRESHNESS_SCORE</p>
<p className={`font-display text-7xl font-bold ${scan.freshness_score >= 85 ? 'text-secondary' : 'text-neon'} print:text-black mb-4`}>
{scan.freshness_score}
</p>
<div className="inline-block border border-outline-variant/30 px-4 py-2 bg-surface-lowest">
<p className="font-mono text-xs tracking-widest uppercase">
GRADE {scan.grade} — {scan.label}
</p>
</div>
</div>

{/* Data Markers */}
<div className="mb-12">
<p className="font-mono text-[0.65rem] tracking-widest text-on-surface-variant mb-4 uppercase">RAW_MARKERS</p>
<pre className="font-mono text-xs bg-surface-low border border-outline-variant/30 p-4 text-on-surface print:bg-white print:border-black whitespace-pre-wrap">
{JSON.stringify(scan.markers, null, 2)}
</pre>
</div>

{/* Controls */}
<div className="flex gap-4 print:hidden">
<button
onClick={handleShare}
className="flex-1 py-3 bg-secondary text-on-primary font-display font-bold text-sm tracking-wider uppercase transition-colors hover:brightness-110 border-none cursor-pointer"
>
{copied ? 'COPIED TO CLIPBOARD' : 'COPY SHARE LINK'}
</button>
<button
onClick={handlePrint}
className="flex-1 py-3 bg-surface-high text-on-surface font-display font-bold text-sm tracking-wider uppercase transition-colors hover:text-neon border border-outline-variant/30 cursor-pointer"
>
PRINT / SAVE PDF
</button>
</div>
</div>
</div>
);
}
19 changes: 18 additions & 1 deletion src/pages/ScannerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function ScannerPage() {
const [uploadPreviewUrl, setUploadPreviewUrl] = useState<string | null>(null);
// Controls whether the camera stream is active
const [cameraActive, setCameraActive] = useState(true);
const [copied, setCopied] = useState(false);

const videoRef = useRef<HTMLVideoElement>(null);
const progressRef = useRef<ReturnType<typeof setInterval> | null>(null);
Expand Down Expand Up @@ -350,7 +351,23 @@ export default function ScannerPage() {
</button>
</div>
)}

{scanComplete && freshness !== null && freshness >= 85 && (
<div className="flex flex-col gap-3 mt-1">
<button
onClick={() => {
const scanId = sessionStorage.getItem('lastScanId');
if (scanId) {
navigator.clipboard.writeText(`${window.location.origin}/report/${scanId}`);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}}
className="w-full py-3 bg-secondary text-on-primary font-[family-name:var(--font-display)] font-bold text-sm tracking-wider cursor-pointer border-none transition-colors hover:brightness-110 flex items-center justify-center gap-2"
>
{copied ? 'COPIED TO CLIPBOARD' : 'SHARE GRADE-A REPORT'}
</button>
</div>
)}
<StatusTerminal
messages={['MODEL: STREAM_DUAL', 'DEVICE: ON_EDGE', 'LATENCY: <50ms']}
className="justify-center"
Expand Down
2 changes: 1 addition & 1 deletion vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"source": "/ingest/:path*",
"destination": "https://us.i.posthog.com/:path*"
},
{ "source": "/(.*)", "destination": "/" }
{ "source": "/(.*)", "destination": "/index.html" }
]
}
Loading