Skip to content
Draft
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
23 changes: 23 additions & 0 deletions ISSUESCX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Vercel verification notes

Checked on 2026-10-25 for commit `b09f3bb547a3b648ac4da94bc908a9f68bd3995b` (`feat/dashboard-visual-recovery`).

## Verified

- Production homepage `https://opencluster.vercel.app/` returned HTTP 200 and served the OpenCluster HTML application shell.
- The production JavaScript bundle and stylesheet referenced by that shell both returned HTTP 200.
- GitHub's commit status API reported `Vercel: success` for the current commit. This confirms Vercel's reported status for the commit; it does not prove the preview UI was opened or visually tested.

## Issues and limits

- The branch preview `https://opencluster-git-feat-dashboard-visual-recovery-zyrix-dev.vercel.app/` is protected by Vercel Authentication and returned HTTP 302 to its protection message. Its rendered application and interactions could not be checked anonymously.
- Vercel deployment inspection returned HTTP 403 because this connection is not authorized for team `zyrix-dev`. A one-time authorization link was generated and requires the user to approve team access; it expires after 10 minutes. The temporary link is intentionally not stored in this repository.
- The GitHub CLI (`gh`) is not installed, so PR/check details could not be queried with it. Public GitHub APIs were used for the commit status instead.
- No browser-based visual QA, runtime interaction check, or build-log inspection was completed as part of this Vercel status check.
- Directly pushing to `main` is disallowed by `docs/GITHUB_WORKFLOW.md` and the repository instructions. Changes must be pushed on a branch and submitted through a PR after verifying the Git push identity and satisfying required checks/reviews. The successful `git ls-remote` only confirmed read access; it did not establish the identity authorized to push.

## Next steps

1. Approve the Vercel team-access link provided by the assistant, then retry deployment inspection for the commit above.
2. Use the authorized preview URL to inspect the rendered page and interactions.
3. Keep repository changes on a branch and use the required PR/review workflow; do not push directly to `main`.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ For repository contribution setup, read [AGENTS.md](AGENTS.md), the [development
- [Product requirements](docs/PRD.md) · [Roadmap](docs/ROADMAP.md) · [Architecture proposal](docs/ARCHITECTURE.md)
- [Technology stack](docs/TECH_STACK.md) · [Design system](docs/DESIGN_SYSTEM.md)
- [Dashboard motion and performance](docs/DASHBOARD_PERFORMANCE.md)
- [Topology visualization and Z.Y.R.I.X particle core](docs/TOPOLOGY_VISUALIZATION.md)
- [Dashboard product requirements](docs/DASHBOARD_PRD.md) · [UI/UX specification](docs/DASHBOARD_UI_UX_SPEC.md) · [Architecture and component map](docs/DASHBOARD_ARCHITECTURE_AND_COMPONENT_MAP.md)
- [API design proposal](docs/API_DESIGN.md) · [Database design proposal](docs/DATABASE_DESIGN.md) · [Node protocol proposal](docs/NODE_PROTOCOL.md)
- [Security architecture proposal](docs/SECURITY_ARCHITECTURE.md) · [Development](docs/DEVELOPMENT.md) · [Testing strategy](docs/TESTING_STRATEGY.md)
Expand Down
51 changes: 51 additions & 0 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
},
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@fontsource-variable/sora": "^5.3.0",
"@gsap/react": "^2.1.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
Expand All @@ -33,6 +35,8 @@
"idb": "^8.0.4",
"lenis": "^1.3.27",
"lucide-react": "^1.61.0",
"motion": "^14.6.0",
"ogl": "^1.0.11",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"react-router-dom": "^7.18.4",
Expand Down
23 changes: 23 additions & 0 deletions dashboard/src/animations/motion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Transition, Variants } from "framer-motion";

export const motionTiming = {
card: 0.38,
chart: 0.76,
page: 0.32,
stagger: 0.055,
} as const;

export const motionEase = {
interface: [0.23, 1, 0.32, 1] as const,
exit: [0.4, 0, 1, 1] as const,
};

export const pageVariants: Variants = {
initial: { opacity: 0, y: 9, filter: "blur(2px)" },
enter: { opacity: 1, y: 0, filter: "blur(0px)" },
};

export const pageTransition: Transition = {
duration: motionTiming.page,
ease: motionEase.interface,
};
62 changes: 62 additions & 0 deletions dashboard/src/animations/theme-transition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { flushSync } from "react-dom";

export function transitionTheme(
origin: HTMLElement | null,
next: "light" | "dark",
apply: () => void,
) {
const root = document.documentElement;
const reduced =
window.matchMedia("(prefers-reduced-motion: reduce)").matches ||
root.dataset.motion === "reduced" ||
root.dataset.performance === "on";
const start = () => {
root.dataset.theme = next;
root.style.colorScheme = next;
apply();
};

if (reduced || typeof document.startViewTransition !== "function") {
flushSync(start);
return;
}

const rect = origin?.getBoundingClientRect();
const x = rect ? rect.left + rect.width / 2 : innerWidth / 2;
const y = rect ? rect.top + rect.height / 2 : innerHeight / 2;
const radius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
);
root.dataset.themeTransition = "active";
let transition: ViewTransition;
try {
transition = document.startViewTransition(() => flushSync(start));
} catch {
delete root.dataset.themeTransition;
flushSync(start);
return;
}
transition.finished
.finally(() => {
delete root.dataset.themeTransition;
})
.catch(() => undefined);
transition.ready
.then(() => {
root.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${radius}px at ${x}px ${y}px)`,
],
},
{
duration: 520,
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
pseudoElement: "::view-transition-new(root)",
},
);
})
.catch(() => undefined);
}
62 changes: 56 additions & 6 deletions dashboard/src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Grid2X2,
Layers3,
Menu,
Puzzle,
Search,
Settings,
Sparkles,
Expand All @@ -22,9 +23,12 @@ import {
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import { pageTransition, pageVariants } from "../animations/motion";
import { useDashboardStore } from "../state/useDashboardStore";
import { BranchedMenu } from "./BranchedMenu";
import { Dock } from "./Dock";
import { BellToggle } from "./react-bits/controls";
import { AnimatedThemeToggler } from "./react-bits/utilities";
import { Button } from "./ui";

const navItems = [
Expand All @@ -37,6 +41,7 @@ const navItems = [
{ label: "AI Software", to: "/software", icon: TerminalSquare },
{ label: "Tasks", to: "/tasks", icon: Activity },
{ label: "Monitoring", to: "/monitoring", icon: Gauge },
{ label: "Component Lab", to: "/component-lab", icon: Puzzle },
{ label: "Activity", to: "/activity", icon: Bell },
];
const routeNames: Record<string, string> = {
Expand All @@ -51,6 +56,7 @@ const routeNames: Record<string, string> = {
"/monitoring": "Monitoring",
"/activity": "Activity",
"/settings": "Settings",
"/component-lab": "Component Lab",
};
const branchedItems = [
{ label: "Overview", value: "/", icon: <Grid2X2 size={16} /> },
Expand All @@ -66,6 +72,11 @@ const branchedItems = [
{ value: "/nodes", label: "Nodes", icon: <Cpu size={16} /> },
{ value: "/monitoring", label: "Monitoring", icon: <Gauge size={16} /> },
{ value: "/activity", label: "Activity", icon: <Bell size={16} /> },
{
value: "/component-lab",
label: "Component Lab",
icon: <Puzzle size={16} />,
},
],
},
{
Expand Down Expand Up @@ -113,6 +124,7 @@ export function AppShell() {
const collapsed = useDashboardStore((s) => s.sidebarCollapsed);
const toggleSidebar = useDashboardStore((s) => s.toggleSidebar);
const theme = useDashboardStore((s) => s.theme);
const setTheme = useDashboardStore((s) => s.setTheme);
const demoMotion = useDashboardStore((s) => s.demoMotion);
const compactDashboard = useDashboardStore((s) => s.compactDashboard);
const performanceMode = useDashboardStore((s) => s.performanceMode);
Expand All @@ -123,12 +135,28 @@ export function AppShell() {
(s) => s.clearPersistenceError,
);
const prefersReducedMotion = useReducedMotion();
const [systemTheme, setSystemTheme] = useState<"light" | "dark">("light");
const [searchOpen, setSearchOpen] = useState(false);
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
const [query, setQuery] = useState("");
const searchInput = useRef<HTMLInputElement>(null);
useEffect(() => {
const readPreference = () =>
setNotificationsEnabled(
localStorage.getItem("opencluster.notifications") !== "false",
);
readPreference();
window.addEventListener("opencluster:notifications-change", readPreference);
return () =>
window.removeEventListener(
"opencluster:notifications-change",
readPreference,
);
}, []);
useEffect(() => {
const media = window.matchMedia("(prefers-color-scheme: light)");
const updateTheme = () => {
setSystemTheme(media.matches ? "light" : "dark");
document.documentElement.dataset.theme =
theme === "system" ? (media.matches ? "light" : "dark") : theme;
};
Expand Down Expand Up @@ -292,6 +320,12 @@ export function AppShell() {
<strong>{routeNames[location.pathname] ?? "Dashboard"}</strong>
</div>
<div className="top-actions">
<AnimatedThemeToggler
theme={theme === "system" ? systemTheme : theme}
onThemeChange={setTheme}
className="top-theme-toggle"
title="Change dashboard theme"
/>
<Button
variant="secondary"
className="search-trigger"
Expand All @@ -305,12 +339,27 @@ export function AppShell() {
</Button>
<Button
variant="ghost"
aria-label="Notifications"
aria-label="Activity"
onClick={() => navigate("/activity")}
>
<Bell size={17} />
<span className="notification-dot" />
<Activity size={17} />
{notificationsEnabled && <span className="notification-dot" />}
</Button>
<BellToggle
checked={notificationsEnabled}
onCheckedChange={(enabled) => {
localStorage.setItem(
"opencluster.notifications",
String(enabled),
);
setNotificationsEnabled(enabled);
window.dispatchEvent(
new Event("opencluster:notifications-change"),
);
}}
label="Activity notifications"
className="top-notification-toggle"
/>
<span className="top-divider" />
<div className="top-avatar" title="Demo operator">
OC
Expand Down Expand Up @@ -338,11 +387,12 @@ export function AppShell() {
className="page-transition"
initial={
demoMotion && !performanceMode && !prefersReducedMotion
? { opacity: 0, y: 8 }
? "initial"
: false
}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.24, ease: "easeOut" }}
animate="enter"
variants={pageVariants}
transition={pageTransition}
>
<Outlet />
</motion.div>
Expand Down
Loading
Loading