feat: establish NekoNest Cloud control and relay

This commit is contained in:
2026-08-12 23:25:43 +08:00
commit f27606b709
222 changed files with 71456 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
"use client";
import { useSyncExternalStore } from "react";
function subscribeToConnectivity(onStoreChange: () => void) {
const handleOnline = () => onStoreChange();
const handleOffline = () => onStoreChange();
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}
function getOfflineSnapshot() {
return !navigator.onLine;
}
function getServerOfflineSnapshot() {
return false;
}
export function ConnectivityBanner() {
const offline = useSyncExternalStore(
subscribeToConnectivity,
getOfflineSnapshot,
getServerOfflineSnapshot,
);
if (!offline) return null;
return (
<div className="connectivity-banner" role="status" aria-live="polite">
<strong>线</strong>
<span>
</span>
</div>
);
}
+67
View File
@@ -0,0 +1,67 @@
"use client";
import Link from "next/link";
export function RouteLoadingState({
area,
description,
}: {
area: string;
description: string;
}) {
return (
<main className="route-state-shell" aria-busy="true" aria-live="polite">
<section className="route-state-card route-loading-card">
<span className="route-state-kicker">{area}</span>
<h1></h1>
<p>{description}</p>
<div className="route-loading-lines" aria-hidden="true">
<span />
<span />
<span />
</div>
</section>
</main>
);
}
export function RouteErrorState({
area,
title,
description,
reset,
dashboard = false,
}: {
area: string;
title: string;
description: string;
reset: () => void;
dashboard?: boolean;
}) {
return (
<main className="route-state-shell">
<section className="route-state-card route-error-card" role="alert">
<span className="route-state-kicker">{area}</span>
<h1>{title}</h1>
<p>{description}</p>
<p className="route-state-note">
线
</p>
<div className="route-state-actions">
<button className="button button-primary" type="button" onClick={reset}>
</button>
<Link className="button button-secondary" href="/status">
</Link>
<Link
className="route-state-link"
href={dashboard ? "/dashboard/feedback" : "/"}
>
{dashboard ? "仍有问题,提交反馈" : "返回首页"}
</Link>
</div>
</section>
</main>
);
}
+257
View File
@@ -0,0 +1,257 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { chatGPTSignInPath, chatGPTSignOutPath } from "../chatgpt-auth";
import { getCloudViewer, type CloudViewer } from "../cloud-auth";
import {
getActiveBeta,
getServiceStatusSnapshot,
type ServiceStatusSnapshot,
} from "@/db/repository";
import { ConnectivityBanner } from "./ConnectivityBanner";
const serviceStatusLabels = {
operational: "服务正常",
maintenance: "计划维护",
degraded: "服务降级",
outage: "服务中断",
} as const;
export function Brand({ compact = false }: { compact?: boolean }) {
return (
<span className="brand-lockup">
<span className="brand-mark" aria-hidden="true">
<svg viewBox="0 0 40 40" role="img">
<path d="M8 16 5 6l11 6h8L35 6l-3 10v10c0 6-5 10-12 10S8 32 8 26V16Z" />
<path d="M14 23h.1M26 23h.1M16 29c2.4 1.8 5.6 1.8 8 0" />
</svg>
</span>
{!compact && (
<span className="brand-type">
<strong>NekoNest</strong>
<span>Cloud</span>
</span>
)}
</span>
);
}
export function StatusPill({
tone = "neutral",
children,
}: {
tone?: "good" | "warn" | "danger" | "neutral" | "info";
children: ReactNode;
}) {
return <span className={`status-pill status-${tone}`}>{children}</span>;
}
export async function PublicShell({
children,
serviceStatus: providedServiceStatus,
}: {
children: ReactNode;
serviceStatus?: ServiceStatusSnapshot;
}) {
const [viewer, beta, serviceStatus] = await Promise.all([
getCloudViewer(),
getActiveBeta(),
providedServiceStatus ?? getServiceStatusSnapshot(),
]);
return (
<div className="site-shell">
<header className="public-header">
<div className="public-header-inner">
<Link className="brand-link" href="/" aria-label="NekoNest Cloud 首页">
<Brand />
</Link>
<nav className="public-nav" aria-label="主要导航">
<Link href="/#how"></Link>
<Link href="/download"></Link>
<Link href="/pricing"></Link>
<Link href="/trust"></Link>
<Link href="/privacy"></Link>
<Link href="/readiness">线</Link>
<Link href="/status"></Link>
</nav>
<div className="header-actions">
<span className="beta-chip"> · {beta ? "公测免费" : "公测已结束"}</span>
{viewer ? (
<Link className="button button-small button-primary" href="/dashboard">
</Link>
) : (
<Link
className="button button-small button-primary"
href={chatGPTSignInPath("/dashboard")}
>
</Link>
)}
</div>
</div>
</header>
<ConnectivityBanner />
<main>{children}</main>
<footer className="public-footer">
<div className="footer-grid">
<div>
<Brand />
<p> coding-agent</p>
</div>
<div>
<strong></strong>
<Link href="/download"></Link>
<Link href="/pricing"></Link>
<Link href="/trust"></Link>
<Link href="/privacy"></Link>
<Link href="/readiness">线</Link>
<Link href="/status"></Link>
</div>
<div>
<strong></strong>
<p>Cloud Token</p>
</div>
<div>
<strong></strong>
<p></p>
</div>
</div>
<div className="footer-bottom">
<span>© 2026 NekoNest Cloud prototype</span>
<span>{serviceStatusLabels[serviceStatus.status]} · </span>
</div>
</footer>
</div>
);
}
const dashboardNav = [
{ href: "/dashboard", label: "总览", icon: "⌂" },
{ href: "/dashboard/hosts", label: "主机", icon: "▣" },
{ href: "/dashboard/billing", label: "公测权益", icon: "○" },
{ href: "/dashboard/security", label: "安全与设备", icon: "◇" },
{ href: "/dashboard/feedback", label: "问题反馈", icon: "?" },
] as const;
export async function DashboardShell({
viewer,
active,
children,
serviceStatus: providedServiceStatus,
}: {
viewer: CloudViewer;
active: string;
children: ReactNode;
serviceStatus?: ServiceStatusSnapshot;
}) {
const serviceStatus =
providedServiceStatus ?? (await getServiceStatusSnapshot());
return (
<div className="cloud-shell">
<aside className="cloud-sidebar">
<Link className="brand-link cloud-brand" href="/">
<Brand />
</Link>
<div className="prototype-notice">
<span className="notice-dot" />
</div>
<nav className="cloud-nav" aria-label="控制台导航">
{dashboardNav.map((item) => (
<Link
className={active === item.href ? "active" : undefined}
href={item.href}
key={item.href}
>
<span aria-hidden="true">{item.icon}</span>
{item.label}
</Link>
))}
{viewer.isAdmin && (
<Link className={active === "/admin" ? "active" : undefined} href="/admin">
<span aria-hidden="true"></span>
</Link>
)}
</nav>
<div className="sidebar-account">
<span className="account-avatar" aria-hidden="true">
{viewer.displayName.slice(0, 1).toUpperCase()}
</span>
<span>
<strong>{viewer.displayName}</strong>
<small>{viewer.isLocalDemo ? "本地演示身份" : viewer.email}</small>
</span>
{!viewer.isLocalDemo && (
<Link href={chatGPTSignOutPath("/")} aria-label="退出登录">
</Link>
)}
</div>
</aside>
<div className="cloud-main">
<ConnectivityBanner />
{serviceStatus.activeIncidents.length > 0 && (
<div
className={`service-incident-banner incident-${serviceStatus.status}`}
role="status"
>
<span>
<strong>{serviceStatusLabels[serviceStatus.status]}</strong>
{serviceStatus.activeIncidents[0].title}
</span>
<Link href="/status"> </Link>
</div>
)}
{viewer.isLocalDemo && (
<div className="demo-banner" role="status">
使线
</div>
)}
{children}
</div>
</div>
);
}
export function PageHeading({
eyebrow,
title,
description,
actions,
}: {
eyebrow?: string;
title: string;
description: string;
actions?: ReactNode;
}) {
return (
<header className="page-heading">
<div>
{eyebrow && <span className="eyebrow">{eyebrow}</span>}
<h1>{title}</h1>
<p>{description}</p>
</div>
{actions && <div className="page-actions">{actions}</div>}
</header>
);
}
export function formatMoney(amountMinor: number, currency = "CNY") {
return new Intl.NumberFormat("zh-CN", {
style: "currency",
currency,
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(amountMinor / 100);
}
export function formatDate(value: string | null, includeTime = false) {
if (!value) return "未设定";
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "short",
day: "numeric",
...(includeTime ? { hour: "2-digit", minute: "2-digit" } : {}),
}).format(new Date(value));
}