diff --git a/apps/web/src/app/api/copilot/route.ts b/apps/web/src/app/api/copilot/route.ts
index 4ceea85..07f1dd2 100644
--- a/apps/web/src/app/api/copilot/route.ts
+++ b/apps/web/src/app/api/copilot/route.ts
@@ -136,6 +136,12 @@ ${signalsContext}`;
});
const data = await res.json();
+ if (!res.ok || data?.type === 'error') {
+ console.error('Anthropic API error:', JSON.stringify(data));
+ return NextResponse.json({
+ reply: `Iris error: ${data?.error?.message ?? data?.error?.type ?? 'Unknown error from Anthropic API'}`,
+ });
+ }
const reply = data?.content?.[0]?.text ?? 'No response.';
return NextResponse.json({ reply });
}
diff --git a/apps/web/src/app/api/login/route.ts b/apps/web/src/app/api/login/route.ts
new file mode 100644
index 0000000..916a3d5
--- /dev/null
+++ b/apps/web/src/app/api/login/route.ts
@@ -0,0 +1,22 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const PASSWORD = 'adonis2026';
+const COOKIE_NAME = 'adonis_auth';
+
+export async function POST(req: NextRequest) {
+ const { password } = await req.json();
+
+ if (password !== PASSWORD) {
+ return NextResponse.json({ error: 'Incorrect password' }, { status: 401 });
+ }
+
+ const res = NextResponse.json({ ok: true });
+ res.cookies.set(COOKIE_NAME, PASSWORD, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ maxAge: 60 * 60 * 24 * 30, // 30 days
+ path: '/',
+ });
+ return res;
+}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 9962c9a..ad3ea82 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -1,12 +1,8 @@
import './globals.css';
-import { Suspense } from 'react';
import type { Metadata } from 'next';
import Nav from '@/components/Nav';
-import CoPilot from '@/components/CoPilot';
-import MobileNav from '@/components/MobileNav';
-import DigestTracker from '@/components/DigestTracker';
-import UserProvider from '@/components/UserProvider';
import SidebarUser from '@/components/SidebarUser';
+import ShellGuard from '@/components/ShellGuard';
export const metadata: Metadata = {
title: 'Adonis Account Intelligence',
@@ -17,18 +13,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
- {/* Mobile top bar */}
-
-
-
-
- {children}
-
-
-
-
-
-
+ }>{children}
);
diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx
new file mode 100644
index 0000000..2782a32
--- /dev/null
+++ b/apps/web/src/app/login/page.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+
+export default function LoginPage() {
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+ const router = useRouter();
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setLoading(true);
+ setError('');
+
+ const res = await fetch('/api/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ password }),
+ });
+
+ if (res.ok) {
+ router.push('/');
+ router.refresh();
+ } else {
+ setError('Incorrect password.');
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+ Account Intel
+
+
+ for Adonis · Healthcare Revenue-Cycle Software
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 504c8f2..a29c3a8 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -29,9 +29,15 @@ export default async function HomePage({ searchParams }: PageProps) {
const hospitalMap = Object.fromEntries(hospitals.map((h) => [h.id, h.name]));
// Strip garbage — filtered_out signals should never appear in the feed
- const cleanSignals = allSignals.filter(
- (s) => s.tier !== 'filtered_out' && s.signal_type !== 'filtered_out'
- );
+ const seenUrls = new Set();
+ const cleanSignals = allSignals.filter((s) => {
+ if (s.tier === 'filtered_out' || s.signal_type === 'filtered_out') return false;
+ if (s.source_url) {
+ if (seenUrls.has(s.source_url)) return false;
+ seenUrls.add(s.source_url);
+ }
+ return true;
+ });
// Filter by category
let signals = category
diff --git a/apps/web/src/app/review/page.tsx b/apps/web/src/app/review/page.tsx
index 877eacb..9ba237a 100644
--- a/apps/web/src/app/review/page.tsx
+++ b/apps/web/src/app/review/page.tsx
@@ -20,7 +20,7 @@ export default async function ReviewPage() {
return (
- Review queue
+ Review Queue
Low-confidence signals (under 70%) awaiting Danielle's approval before digest.
{signals.length > 0 && (
diff --git a/apps/web/src/components/CoPilot.tsx b/apps/web/src/components/CoPilot.tsx
index 00a81ee..a502542 100644
--- a/apps/web/src/components/CoPilot.tsx
+++ b/apps/web/src/components/CoPilot.tsx
@@ -13,6 +13,7 @@ function IrisMessage({ text }: { text: string }) {
const lines = text.split('\n');
const nodes: React.ReactNode[] = [];
let i = 0;
+ let k = 0;
while (i < lines.length) {
const line = lines[i] ?? '';
@@ -31,7 +32,7 @@ function IrisMessage({ text }: { text: string }) {
i++;
}
nodes.push(
-
+
{items.map((item, j) => (
{renderInline(item)}
))}
@@ -48,7 +49,7 @@ function IrisMessage({ text }: { text: string }) {
i++;
}
nodes.push(
-
+
{items.map((item, j) => (
{renderInline(item)}
))}
@@ -59,7 +60,7 @@ function IrisMessage({ text }: { text: string }) {
// Regular paragraph
nodes.push(
-
+
{renderInline(line)}
);
@@ -131,7 +132,7 @@ function defaultPos() {
const isMobile = window.innerWidth < 768;
return {
x: window.innerWidth - BUBBLE_SIZE - (isMobile ? 16 : 24),
- y: window.innerHeight - BUBBLE_SIZE - (isMobile ? 88 : 24),
+ y: window.innerHeight - BUBBLE_SIZE - (isMobile ? 96 : 48),
};
}
@@ -146,7 +147,7 @@ export default function CoPilot() {
const [pulsing, setPulsing] = useState(false);
const [hasUnread, setHasUnread] = useState(false);
// null = not yet mounted (SSR safe)
- const { userId, isAdmin } = useUser();
+ const { userId, isAdmin, userName } = useUser();
const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
const bottomRef = useRef(null);
const pillDismissTimer = useRef | null>(null);
@@ -170,11 +171,11 @@ export default function CoPilot() {
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) drag.current.moved = true;
const newX = Math.max(
0,
- Math.min(window.innerWidth - BUBBLE_SIZE, drag.current.startPosX + dx)
+ Math.min(window.innerWidth - BUBBLE_SIZE - 8, drag.current.startPosX + dx)
);
const newY = Math.max(
0,
- Math.min(window.innerHeight - BUBBLE_SIZE, drag.current.startPosY + dy)
+ Math.min(window.innerHeight - BUBBLE_SIZE - 16, drag.current.startPosY + dy)
);
setPos({ x: newX, y: newY });
}
@@ -185,8 +186,8 @@ export default function CoPilot() {
setPos((prev) => {
if (!prev) return defaultPos();
return {
- x: Math.max(0, Math.min(window.innerWidth - BUBBLE_SIZE, prev.x)),
- y: Math.max(0, Math.min(window.innerHeight - BUBBLE_SIZE, prev.y)),
+ x: Math.max(0, Math.min(window.innerWidth - BUBBLE_SIZE - 8, prev.x)),
+ y: Math.max(0, Math.min(window.innerHeight - BUBBLE_SIZE - 16, prev.y)),
};
});
}
@@ -338,12 +339,10 @@ export default function CoPilot() {
};
}
- const gap = 8;
- let top = pos.y - PANEL_HEIGHT - gap;
- if (top < 8) top = pos.y + BUBBLE_SIZE + gap;
- let left = pos.x + BUBBLE_SIZE - PANEL_WIDTH;
- left = Math.max(8, Math.min(window.innerWidth - PANEL_WIDTH - 8, left));
- return { position: 'fixed', top, left, zIndex: 50, width: PANEL_WIDTH };
+ const gap = 12;
+ const bottom = window.innerHeight - pos.y + gap;
+ const right = Math.max(8, window.innerWidth - pos.x - BUBBLE_SIZE);
+ return { position: 'fixed', bottom, right, zIndex: 50, width: PANEL_WIDTH };
}
if (!pos) return null;
@@ -534,6 +533,11 @@ export default function CoPilot() {
>
{messages.length === 0 && (
+ {userName && (
+
+ Hi {userName.split(' ')[0]}, what do you need?
+
+ )}
handleSend(
@@ -545,11 +549,18 @@ export default function CoPilot() {
>
Brief me on my territory
- {[
- 'Who should I call this week?',
- 'Draft an outreach email for Ascension',
- "What's urgent across my accounts?",
- ].map((prompt) => (
+ {(isAdmin
+ ? [
+ 'Draft my weekly digest',
+ "What's urgent across all accounts?",
+ 'Which AEs need a follow-up this week?',
+ ]
+ : [
+ 'Who should I call this week?',
+ 'Summarize my most important account',
+ "What's urgent across my accounts?",
+ ]
+ ).map((prompt) => (
handleSend(prompt)}
diff --git a/apps/web/src/components/ShellGuard.tsx b/apps/web/src/components/ShellGuard.tsx
new file mode 100644
index 0000000..a322bea
--- /dev/null
+++ b/apps/web/src/components/ShellGuard.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+import { Suspense } from 'react';
+import CoPilot from '@/components/CoPilot';
+import MobileNav from '@/components/MobileNav';
+import DigestTracker from '@/components/DigestTracker';
+import UserProvider from '@/components/UserProvider';
+
+export default function ShellGuard({
+ children,
+ sidebar,
+}: {
+ children: React.ReactNode;
+ sidebar: React.ReactNode;
+}) {
+ const pathname = usePathname();
+
+ if (pathname === '/login') {
+ return <>{children}>;
+ }
+
+ return (
+
+
+
+ {sidebar}
+ {children}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts
new file mode 100644
index 0000000..60b176b
--- /dev/null
+++ b/apps/web/src/middleware.ts
@@ -0,0 +1,26 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+const PASSWORD = 'adonis2026';
+const COOKIE_NAME = 'adonis_auth';
+
+export function middleware(req: NextRequest) {
+ const { pathname } = req.nextUrl;
+
+ // Allow login page and its API route through
+ if (pathname === '/login' || pathname === '/api/login') {
+ return NextResponse.next();
+ }
+
+ const cookie = req.cookies.get(COOKIE_NAME);
+ if (cookie?.value === PASSWORD) {
+ return NextResponse.next();
+ }
+
+ const loginUrl = req.nextUrl.clone();
+ loginUrl.pathname = '/login';
+ return NextResponse.redirect(loginUrl);
+}
+
+export const config = {
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
+};