diff --git a/app/mission-control/pdf-viewer/page.tsx b/app/mission-control/pdf-viewer/page.tsx
index d7a4cfd..30baf0b 100644
--- a/app/mission-control/pdf-viewer/page.tsx
+++ b/app/mission-control/pdf-viewer/page.tsx
@@ -1,69 +1,22 @@
"use client";
import BackToMC from "@/components/mission-control/BackToMC";
-import { useState } from "react";
+import dynamic from "next/dynamic";
+
+// Dynamically import PDFViewer with SSR disabled
+const PDFViewer = dynamic(() => import("@/components/mission-control/PDFViewerClient"), {
+ ssr: false,
+ loading: () => (
+
+
+
⏳
+
Loading PDF viewer...
+
+
+ ),
+});
export default function PdfViewerPage() {
- const [pdfData, setPdfData] = useState(null);
- const [pdfName, setPdfName] = useState("");
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [viewMode, setViewMode] = useState<"embed" | "download">("embed");
-
- const handleFileUpload = async (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file) return;
-
- if (file.type !== "application/pdf") {
- setError("Please select a PDF file");
- return;
- }
-
- setLoading(true);
- setError(null);
- setPdfName(file.name);
-
- const reader = new FileReader();
- reader.onload = (event) => {
- const base64 = event.target?.result as string;
- setPdfData(base64);
- setLoading(false);
- };
- reader.onerror = () => {
- setError("Failed to read file");
- setLoading(false);
- };
- reader.readAsDataURL(file);
- };
-
- const handleUrlSubmit = async () => {
- const input = prompt("Enter PDF URL:");
- if (!input) return;
-
- setLoading(true);
- setError(null);
-
- try {
- new URL(input);
- // Use Google Docs viewer for external URLs
- const googleViewerUrl = `https://docs.google.com/gview?url=${encodeURIComponent(input)}&embedded=true`;
- setPdfData(googleViewerUrl);
- setPdfName(input.split("/").pop() || "document.pdf");
- setLoading(false);
- } catch {
- setError("Invalid URL");
- setLoading(false);
- }
- };
-
- const handleDownload = () => {
- if (!pdfData) return;
- const link = document.createElement("a");
- link.href = pdfData;
- link.download = pdfName || "document.pdf";
- link.click();
- };
-
return (
{/* Header */}
@@ -72,131 +25,9 @@ export default function PdfViewerPage() {
View and analyze PDF documents
- {/* Toolbar */}
-
-
-
-
-
-
- {pdfData && (
- <>
-
-
- {pdfData.startsWith("data:") && (
-
- ↗ Open in New Tab
-
- )}
- >
- )}
-
-
-
{/* Content */}
-
- {error && (
-
-
Error
-
{error}
-
-
- )}
-
- {!pdfData && !loading && (
-
-
-
📄
-
No PDF Loaded
-
Upload a PDF or enter a URL to view it
-
-
• Upload your resume to let Horus analyze it
-
• Supports PDF files up to 50MB
-
• Or load from any public PDF URL
-
-
-
- )}
-
- {loading && (
-
- )}
-
- {pdfData && !loading && (
-
- {pdfData.startsWith("data:") ? (
- // For uploaded files, show download prompt since browser PDF rendering in iframe is blocked
-
-
📄
-
{pdfName}
-
PDF loaded successfully
-
-
- ) : (
- // For external URLs, use Google Docs viewer
-
- )}
-
- )}
-
-
- {/* Instructions */}
-
-
- 💡 Tip: Upload your resume PDF and Horus can analyze the design to recreate it
-
+
);
diff --git a/components/mission-control/PDFViewerClient.tsx b/components/mission-control/PDFViewerClient.tsx
new file mode 100644
index 0000000..4c80405
--- /dev/null
+++ b/components/mission-control/PDFViewerClient.tsx
@@ -0,0 +1,241 @@
+"use client";
+
+import { useState, useRef, useEffect } from "react";
+import * as pdfjsLib from "pdfjs-dist";
+
+// Set worker source - use CDN
+pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;
+
+export default function PDFViewerClient() {
+ const [pdfData, setPdfData] = useState(null);
+ const [pdfName, setPdfName] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [numPages, setNumPages] = useState(0);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [scale, setScale] = useState(1.5);
+ const canvasRef = useRef(null);
+ const pdfDocRef = useRef(null);
+
+ const renderPage = async (pageNum: number) => {
+ if (!pdfDocRef.current || !canvasRef.current) return;
+
+ try {
+ const page = await pdfDocRef.current.getPage(pageNum);
+ const viewport = page.getViewport({ scale });
+ const canvas = canvasRef.current;
+ const context = canvas.getContext("2d");
+
+ canvas.height = viewport.height;
+ canvas.width = viewport.width;
+
+ await page.render({
+ canvasContext: context,
+ viewport: viewport,
+ }).promise;
+ } catch (err) {
+ console.error("Error rendering page:", err);
+ }
+ };
+
+ useEffect(() => {
+ if (pdfDocRef.current && currentPage) {
+ renderPage(currentPage);
+ }
+ }, [currentPage, scale]);
+
+ const handleFileUpload = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ if (file.type !== "application/pdf") {
+ setError("Please select a PDF file");
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+ setPdfName(file.name);
+ setCurrentPage(1);
+
+ try {
+ const arrayBuffer = await file.arrayBuffer();
+ const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
+ pdfDocRef.current = pdf;
+ setNumPages(pdf.numPages);
+ setPdfData("local");
+ await renderPage(1);
+ } catch (err) {
+ setError("Failed to load PDF");
+ console.error(err);
+ }
+ setLoading(false);
+ };
+
+ const handleUrlSubmit = async () => {
+ const input = prompt("Enter PDF URL:");
+ if (!input) return;
+
+ setLoading(true);
+ setError(null);
+ setCurrentPage(1);
+
+ try {
+ new URL(input);
+ setPdfName(input.split("/").pop() || "document.pdf");
+
+ const response = await fetch(input);
+ const arrayBuffer = await response.arrayBuffer();
+ const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
+ pdfDocRef.current = pdf;
+ setNumPages(pdf.numPages);
+ setPdfData("url");
+ await renderPage(1);
+ } catch (err) {
+ setError("Failed to load PDF from URL. Make sure the URL is publicly accessible.");
+ console.error(err);
+ }
+ setLoading(false);
+ };
+
+ const goToPrevPage = () => {
+ if (currentPage > 1) {
+ setCurrentPage(currentPage - 1);
+ }
+ };
+
+ const goToNextPage = () => {
+ if (currentPage < numPages) {
+ setCurrentPage(currentPage + 1);
+ }
+ };
+
+ const zoomIn = () => setScale(scale + 0.25);
+ const zoomOut = () => setScale(Math.max(0.5, scale - 0.25));
+
+ return (
+
+ {/* Toolbar */}
+
+
+
+
+
+
+ {pdfData && (
+ <>
+
+
+ {/* Page navigation */}
+
+
+ Page {currentPage} of {numPages}
+
+
+
+
+
+ {/* Zoom controls */}
+
+
+ {Math.round(scale * 100)}%
+
+
+ >
+ )}
+
+
+
+ {/* Content */}
+
+ {error && (
+
+
Error
+
{error}
+
+
+ )}
+
+ {!pdfData && !loading && (
+
+
+
📄
+
No PDF Loaded
+
Upload a PDF or enter a URL to view it
+
+
• Upload your resume to let Horus analyze it
+
• Supports PDF files up to 50MB
+
• Or load from any public PDF URL
+
+
+
+ )}
+
+ {loading && (
+
+ )}
+
+ {pdfData && !loading && (
+
+
+
+ )}
+
+
+ {/* Instructions */}
+
+
+ 💡 Tip: Upload your resume PDF and Horus can analyze the design to recreate it
+
+
+
+ );
+}
diff --git a/data/eod_briefs.json b/data/eod_briefs.json
index bbedc03..cd0924d 100644
--- a/data/eod_briefs.json
+++ b/data/eod_briefs.json
@@ -1,4 +1,16 @@
[
+ {
+ "id": "eod-1774296002482",
+ "date": "2026-03-23",
+ "completed": [
+ "See Mission Control for details",
+ "🛡️ SECURITY: 10 advisories found today! Check MC."
+ ],
+ "progress": {},
+ "council": {},
+ "tomorrow": [],
+ "created_at": "2026-03-23T20:00:02.482Z"
+ },
{
"id": "eod-1774209602462",
"date": "2026-03-22",
diff --git a/package-lock.json b/package-lock.json
index 1bbb626..3ec5c49 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"framer-motion": "^12.23.12",
"lightweight-charts": "^5.1.0",
"next": "^15.5.3",
+ "pdfjs-dist": "^5.5.207",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-pdf": "^10.4.1",
@@ -3484,6 +3485,13 @@
"url": "https://opencollective.com/node-fetch"
}
},
+ "node_modules/node-readable-to-web-readable-stream": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
+ "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -3639,15 +3647,16 @@
"license": "ISC"
},
"node_modules/pdfjs-dist": {
- "version": "5.4.296",
- "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
- "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
+ "version": "5.5.207",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz",
+ "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==",
"license": "Apache-2.0",
"engines": {
- "node": ">=20.16.0 || >=22.3.0"
+ "node": ">=20.19.0 || >=22.13.0 || >=24"
},
"optionalDependencies": {
- "@napi-rs/canvas": "^0.1.80"
+ "@napi-rs/canvas": "^0.1.95",
+ "node-readable-to-web-readable-stream": "^0.4.2"
}
},
"node_modules/picocolors": {
@@ -3984,6 +3993,18 @@
}
}
},
+ "node_modules/react-pdf/node_modules/pdfjs-dist": {
+ "version": "5.4.296",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
+ "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.16.0 || >=22.3.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^0.1.80"
+ }
+ },
"node_modules/react-use-measure": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
diff --git a/package.json b/package.json
index 09f4373..e58e882 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"framer-motion": "^12.23.12",
"lightweight-charts": "^5.1.0",
"next": "^15.5.3",
+ "pdfjs-dist": "^5.5.207",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-pdf": "^10.4.1",