flatten app to repo root and update docker deployment for single-stack runtime
This commit is contained in:
45
components/auth/auth-shell.tsx
Normal file
45
components/auth/auth-shell.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
type AuthShellProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children: React.ReactNode;
|
||||
footer: React.ReactNode;
|
||||
};
|
||||
|
||||
export function AuthShell({ title, subtitle, children, footer }: AuthShellProps) {
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="ambient-grid" aria-hidden="true" />
|
||||
<div className="noise-layer" aria-hidden="true" />
|
||||
|
||||
<div className="relative z-10 mx-auto flex min-h-screen w-full max-w-5xl flex-col justify-center gap-8 px-4 py-10 md:px-8 lg:flex-row lg:items-center">
|
||||
<section className="rounded-2xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] p-6 lg:w-[42%]">
|
||||
<p className="terminal-caption text-xs uppercase tracking-[0.3em] text-[color:var(--terminal-muted)]">Fiscal Clone</p>
|
||||
<h1 className="mt-3 text-3xl font-semibold text-[color:var(--terminal-bright)]">Autonomous Analyst Desk</h1>
|
||||
<p className="mt-3 text-sm leading-6 text-[color:var(--terminal-muted)]">
|
||||
Secure entry into filings intelligence, portfolio diagnostics, and async AI workflows connected to OpenClaw/ZeroClaw.
|
||||
</p>
|
||||
<Link
|
||||
href="https://www.sec.gov/"
|
||||
target="_blank"
|
||||
className="mt-6 inline-flex text-xs uppercase tracking-[0.2em] text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
|
||||
>
|
||||
SEC Data Backbone
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] p-6 shadow-[0_20px_60px_rgba(1,4,10,0.55)] lg:w-[58%]">
|
||||
<h2 className="text-2xl font-semibold text-[color:var(--terminal-bright)]">{title}</h2>
|
||||
<p className="mt-2 text-sm text-[color:var(--terminal-muted)]">{subtitle}</p>
|
||||
|
||||
<div className="mt-6">{children}</div>
|
||||
|
||||
<div className="mt-6 border-t border-[color:var(--line-weak)] pt-4 text-sm text-[color:var(--terminal-muted)]">
|
||||
{footer}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
components/dashboard/metric-card.tsx
Normal file
23
components/dashboard/metric-card.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type MetricCardProps = {
|
||||
label: string;
|
||||
value: string;
|
||||
delta?: string;
|
||||
positive?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function MetricCard({ label, value, delta, positive = true, className }: MetricCardProps) {
|
||||
return (
|
||||
<div className={cn('rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-4', className)}>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-[color:var(--terminal-muted)]">{label}</p>
|
||||
<p className="mt-2 text-2xl font-semibold text-[color:var(--terminal-bright)]">{value}</p>
|
||||
{delta ? (
|
||||
<p className={cn('mt-2 text-xs', positive ? 'text-[#96f5bf]' : 'text-[#ff9898]')}>
|
||||
{delta}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
components/dashboard/task-feed.tsx
Normal file
36
components/dashboard/task-feed.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import type { Task } from '@/lib/types';
|
||||
import { StatusPill } from '@/components/ui/status-pill';
|
||||
|
||||
type TaskFeedProps = {
|
||||
tasks: Task[];
|
||||
};
|
||||
|
||||
const taskLabels: Record<Task['task_type'], string> = {
|
||||
sync_filings: 'Sync filings',
|
||||
refresh_prices: 'Refresh prices',
|
||||
analyze_filing: 'Analyze filing',
|
||||
portfolio_insights: 'Portfolio insights'
|
||||
};
|
||||
|
||||
export function TaskFeed({ tasks }: TaskFeedProps) {
|
||||
if (tasks.length === 0) {
|
||||
return <p className="text-sm text-[color:var(--terminal-muted)]">No recent tasks.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="space-y-2">
|
||||
{tasks.slice(0, 8).map((task) => (
|
||||
<li key={task.id} className="flex items-center justify-between rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
|
||||
<div>
|
||||
<p className="text-sm text-[color:var(--terminal-bright)]">{taskLabels[task.task_type]}</p>
|
||||
<p className="text-xs text-[color:var(--terminal-muted)]">
|
||||
{formatDistanceToNow(new Date(task.created_at), { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
<StatusPill status={task.status} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
114
components/shell/app-shell.tsx
Normal file
114
components/shell/app-shell.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Activity, BookOpenText, ChartCandlestick, Eye } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type AppShellProps = {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: '/', label: 'Command Center', icon: Activity },
|
||||
{ href: '/filings', label: 'Filings Stream', icon: BookOpenText },
|
||||
{ href: '/portfolio', label: 'Portfolio Matrix', icon: ChartCandlestick },
|
||||
{ href: '/watchlist', label: 'Watchlist', icon: Eye }
|
||||
];
|
||||
|
||||
export function AppShell({ title, subtitle, actions, children }: AppShellProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="app-surface">
|
||||
<div className="ambient-grid" aria-hidden="true" />
|
||||
<div className="noise-layer" aria-hidden="true" />
|
||||
|
||||
<div className="relative z-10 mx-auto flex min-h-screen w-full max-w-[1300px] gap-6 px-4 pb-12 pt-6 md:px-8">
|
||||
<aside className="hidden w-72 shrink-0 flex-col gap-6 rounded-2xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] p-5 shadow-[0_0_0_1px_rgba(0,255,180,0.06),0_20px_60px_rgba(1,4,10,0.55)] lg:flex">
|
||||
<div>
|
||||
<p className="terminal-caption text-xs uppercase tracking-[0.25em] text-[color:var(--terminal-muted)]">Fiscal Clone</p>
|
||||
<h1 className="mt-2 text-2xl font-semibold text-[color:var(--terminal-bright)]">Neon Desk</h1>
|
||||
<p className="mt-2 text-sm text-[color:var(--terminal-muted)]">
|
||||
Financial intelligence cockpit with durable AI workflows.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<nav className="space-y-2">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = pathname === item.href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-xl border px-3 py-2 text-sm transition-all duration-200',
|
||||
isActive
|
||||
? 'border-[color:var(--line-strong)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)] shadow-[0_0_18px_rgba(0,255,180,0.16)]'
|
||||
: 'border-transparent text-[color:var(--terminal-muted)] hover:border-[color:var(--line-weak)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--terminal-bright)]'
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-3">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-[color:var(--terminal-muted)]">Runtime</p>
|
||||
<p className="mt-1 truncate text-sm text-[color:var(--terminal-bright)]">local operator mode</p>
|
||||
<p className="mt-2 text-xs text-[color:var(--terminal-muted)]">
|
||||
OpenClaw and market data are driven by environment configuration and live API tasks.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1">
|
||||
<header className="mb-6 rounded-2xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] px-6 py-5 shadow-[0_0_0_1px_rgba(0,255,180,0.05),0_14px_40px_rgba(1,4,10,0.5)]">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="terminal-caption text-xs uppercase tracking-[0.3em] text-[color:var(--terminal-muted)]">Live System</p>
|
||||
<h2 className="mt-2 text-2xl font-semibold text-[color:var(--terminal-bright)] md:text-3xl">{title}</h2>
|
||||
{subtitle ? (
|
||||
<p className="mt-1 text-sm text-[color:var(--terminal-muted)]">{subtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav className="mb-6 flex gap-2 overflow-x-auto rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] p-2 lg:hidden">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = pathname === item.href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'inline-flex min-w-fit items-center gap-2 rounded-lg border px-3 py-2 text-xs transition',
|
||||
isActive
|
||||
? 'border-[color:var(--line-strong)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)]'
|
||||
: 'border-transparent text-[color:var(--terminal-muted)] hover:border-[color:var(--line-weak)] hover:bg-[color:var(--panel-soft)]'
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<main className="space-y-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
components/ui/button.tsx
Normal file
27
components/ui/button.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type ButtonVariant = 'primary' | 'ghost' | 'danger' | 'secondary';
|
||||
|
||||
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
variant?: ButtonVariant;
|
||||
};
|
||||
|
||||
const variantMap: Record<ButtonVariant, string> = {
|
||||
primary: 'border-[color:var(--line-strong)] bg-[color:var(--accent)] text-[#001515] hover:bg-[color:var(--accent-strong)]',
|
||||
secondary: 'border-[color:var(--line-weak)] bg-[color:var(--panel-bright)] text-[color:var(--terminal-bright)] hover:border-[color:var(--line-strong)] hover:bg-[color:var(--panel)]',
|
||||
ghost: 'border-[color:var(--line-weak)] bg-transparent text-[color:var(--terminal-bright)] hover:border-[color:var(--line-strong)] hover:bg-[color:var(--panel-soft)]',
|
||||
danger: 'border-[color:var(--danger)] bg-[color:var(--danger-soft)] text-[#ffc9c9] hover:bg-[color:var(--danger)] hover:text-[#1e0d0d]'
|
||||
};
|
||||
|
||||
export function Button({ className, variant = 'primary', ...props }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition duration-200 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
variantMap[variant],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
15
components/ui/input.tsx
Normal file
15
components/ui/input.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export function Input({ className, ...props }: InputProps) {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
'w-full rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2 text-sm text-[color:var(--terminal-bright)] outline-none transition placeholder:text-[color:var(--terminal-muted)] focus:border-[color:var(--line-strong)] focus:shadow-[0_0_0_3px_rgba(0,255,180,0.14)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
31
components/ui/panel.tsx
Normal file
31
components/ui/panel.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type PanelProps = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Panel({ title, subtitle, actions, children, className }: PanelProps) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'rounded-2xl border border-[color:var(--line-weak)] bg-[color:var(--panel)] p-5 shadow-[0_0_0_1px_rgba(0,255,180,0.03),0_12px_30px_rgba(1,4,10,0.45)]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{(title || subtitle || actions) ? (
|
||||
<header className="mb-4 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
{title ? <h3 className="text-base font-semibold text-[color:var(--terminal-bright)]">{title}</h3> : null}
|
||||
{subtitle ? <p className="mt-1 text-sm text-[color:var(--terminal-muted)]">{subtitle}</p> : null}
|
||||
</div>
|
||||
{actions ? <div>{actions}</div> : null}
|
||||
</header>
|
||||
) : null}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
21
components/ui/status-pill.tsx
Normal file
21
components/ui/status-pill.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { TaskStatus } from '@/lib/types';
|
||||
|
||||
type StatusPillProps = {
|
||||
status: TaskStatus;
|
||||
};
|
||||
|
||||
const classes: Record<TaskStatus, string> = {
|
||||
queued: 'border-[#33587a] bg-[#0a2c3f] text-[#7ecaf5]',
|
||||
running: 'border-[#4f7a33] bg-[#0f311d] text-[#99f085]',
|
||||
completed: 'border-[#1a7a53] bg-[#083a2a] text-[#8bf7cb]',
|
||||
failed: 'border-[#8f3d3d] bg-[#431616] text-[#ff9c9c]'
|
||||
};
|
||||
|
||||
export function StatusPill({ status }: StatusPillProps) {
|
||||
return (
|
||||
<span className={cn('inline-flex items-center rounded-full border px-2 py-1 text-xs uppercase tracking-[0.16em]', classes[status])}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user