[codex] Switch dashboard sections to view routing

Switch dashboard navigation from long section scrolling to hash-backed views, preserving Usage as the default operational view and keeping mobile drawer navigation visible.
This commit is contained in:
Eyejoker
2026-04-26 22:03:52 +09:00
committed by GitHub
parent 646bc34372
commit 11ab18280c
2 changed files with 140 additions and 39 deletions

View File

@@ -27,9 +27,28 @@ interface DashboardState {
type UsageRow = DashboardOverview['usage']['rows'][number];
type RiskLevel = 'ok' | 'warn' | 'critical';
type UsageGroup = 'primary' | 'codex';
type DashboardView = 'usage' | 'health' | 'rooms' | 'scheduled';
const REFRESH_INTERVAL_MS = 15_000;
const LOCALE_STORAGE_KEY = 'ejclaw.dashboard.locale';
const DEFAULT_VIEW: DashboardView = 'usage';
function isDashboardView(
value: string | null | undefined,
): value is DashboardView {
return (
value === 'usage' ||
value === 'health' ||
value === 'rooms' ||
value === 'scheduled'
);
}
function readViewFromHash(): DashboardView {
if (typeof window === 'undefined') return DEFAULT_VIEW;
const raw = window.location.hash.replace(/^#\/?/, '');
return isDashboardView(raw) ? raw : DEFAULT_VIEW;
}
function readInitialLocale(): Locale {
const stored =
@@ -110,10 +129,10 @@ function queueLabel(
function navItems(t: Messages) {
return [
{ href: '#overview', label: t.nav.health },
{ href: '#usage', label: t.nav.usage },
{ href: '#rooms', label: t.nav.rooms },
{ href: '#work', label: t.nav.scheduled },
{ href: '#/usage', label: t.nav.usage, view: 'usage' as const },
{ href: '#/health', label: t.nav.health, view: 'health' as const },
{ href: '#/rooms', label: t.nav.rooms, view: 'rooms' as const },
{ href: '#/scheduled', label: t.nav.scheduled, view: 'scheduled' as const },
];
}
@@ -170,15 +189,19 @@ function LoadingSkeleton({ t }: { t: Messages }) {
}
function SideRail({
activeView,
lastRefreshed,
locale,
onNavigate,
onLocaleChange,
onRefresh,
refreshing,
t,
}: {
activeView: DashboardView;
lastRefreshed: string | null;
locale: Locale;
onNavigate: (view: DashboardView) => void;
onLocaleChange: (locale: Locale) => void;
onRefresh: () => void;
refreshing: boolean;
@@ -192,7 +215,13 @@ function SideRail({
</div>
<nav aria-label={t.nav.drawerNavAria}>
{navItems(t).map((item) => (
<a href={item.href} key={item.href}>
<a
aria-current={activeView === item.view ? 'page' : undefined}
className={activeView === item.view ? 'is-active' : undefined}
href={item.href}
key={item.href}
onClick={() => onNavigate(item.view)}
>
{item.label}
</a>
))}
@@ -216,26 +245,33 @@ function SideRail({
}
function SectionNav({
activeView,
drawerOpen,
lastRefreshed,
locale,
onCloseDrawer,
onLocaleChange,
onNavigate,
onOpenDrawer,
refreshing,
onRefresh,
t,
}: {
activeView: DashboardView;
drawerOpen: boolean;
lastRefreshed: string | null;
locale: Locale;
onCloseDrawer: () => void;
onLocaleChange: (locale: Locale) => void;
onNavigate: (view: DashboardView) => void;
onOpenDrawer: () => void;
refreshing: boolean;
onRefresh: () => void;
t: Messages;
}) {
const activeLabel =
navItems(t).find((item) => item.view === activeView)?.label ?? t.nav.usage;
return (
<>
<nav className="section-nav" aria-label={t.nav.aria}>
@@ -251,7 +287,7 @@ function SectionNav({
<span />
<span />
</button>
<strong className="topbar-label">{t.nav.usage}</strong>
<strong className="topbar-label">{activeLabel}</strong>
<button
aria-busy={refreshing}
aria-label={refreshing ? t.actions.refreshing : t.actions.refresh}
@@ -297,7 +333,16 @@ function SectionNav({
</div>
<nav aria-label={t.nav.drawerNavAria}>
{navItems(t).map((item) => (
<a href={item.href} key={item.href} onClick={onCloseDrawer}>
<a
aria-current={activeView === item.view ? 'page' : undefined}
className={activeView === item.view ? 'is-active' : undefined}
href={item.href}
key={item.href}
onClick={() => {
onNavigate(item.view);
onCloseDrawer();
}}
>
{item.label}
</a>
))}
@@ -761,6 +806,7 @@ function App() {
const [refreshing, setRefreshing] = useState(false);
const [lastRefreshed, setLastRefreshed] = useState<string | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [activeView, setActiveView] = useState<DashboardView>(readViewFromHash);
const [locale, setLocale] = useState<Locale>(readInitialLocale);
const t = messages[locale];
@@ -769,6 +815,13 @@ function App() {
window.localStorage.setItem(LOCALE_STORAGE_KEY, nextLocale);
}
function navigateToView(view: DashboardView) {
setActiveView(view);
if (window.location.hash !== `#/${view}`) {
window.location.hash = `/${view}`;
}
}
async function refresh(showSpinner = false) {
if (showSpinner) setRefreshing(true);
try {
@@ -788,6 +841,16 @@ function App() {
document.documentElement.lang = localeTags[locale];
}, [locale]);
useEffect(() => {
function handleHashChange() {
setActiveView(readViewFromHash());
}
handleHashChange();
window.addEventListener('hashchange', handleHashChange);
return () => window.removeEventListener('hashchange', handleHashChange);
}, []);
useEffect(() => {
void refresh();
const id = window.setInterval(() => {
@@ -814,8 +877,10 @@ function App() {
return (
<div className="shell">
<SideRail
activeView={activeView}
lastRefreshed={lastRefreshed}
locale={locale}
onNavigate={navigateToView}
onLocaleChange={setDashboardLocale}
onRefresh={() => void refresh(true)}
refreshing={refreshing}
@@ -823,11 +888,13 @@ function App() {
/>
<main className="dashboard-content">
<SectionNav
activeView={activeView}
drawerOpen={drawerOpen}
lastRefreshed={lastRefreshed}
locale={locale}
onCloseDrawer={() => setDrawerOpen(false)}
onLocaleChange={setDashboardLocale}
onNavigate={navigateToView}
onOpenDrawer={() => setDrawerOpen(true)}
onRefresh={() => void refresh(true)}
refreshing={refreshing}
@@ -846,41 +913,50 @@ function App() {
) : null}
{data ? (
<>
<section className="panel usage-first" id="usage">
<div className="panel-title">
<h2>{t.panels.usage}</h2>
<span>{t.panels.usageWindow}</span>
</div>
<UsagePanel locale={locale} overview={data.overview} t={t} />
</section>
<div className={`view-stack view-${activeView}`}>
{activeView === 'usage' ? (
<>
<section className="panel usage-first" id="usage">
<div className="panel-title">
<h2>{t.panels.usage}</h2>
<span>{t.panels.usageWindow}</span>
</div>
<UsagePanel locale={locale} overview={data.overview} t={t} />
</section>
<ControlRail data={data} t={t} />
</>
) : null}
<ControlRail data={data} t={t} />
{activeView === 'health' ? (
<section className="panel view-panel" id="health">
<div className="panel-title">
<h2>{t.panels.health}</h2>
<span>{t.panels.heartbeat}</span>
</div>
<ServicePanel locale={locale} overview={data.overview} t={t} />
</section>
) : null}
<section className="panel" id="agents">
<div className="panel-title">
<h2>{t.panels.health}</h2>
<span>{t.panels.heartbeat}</span>
</div>
<ServicePanel locale={locale} overview={data.overview} t={t} />
</section>
{activeView === 'rooms' ? (
<section className="panel view-panel" id="rooms">
<div className="panel-title">
<h2>{t.panels.rooms}</h2>
<span>{t.panels.queue}</span>
</div>
<RoomPanel snapshots={data.snapshots} t={t} />
</section>
) : null}
<section className="panel" id="rooms">
<div className="panel-title">
<h2>{t.panels.rooms}</h2>
<span>{t.panels.queue}</span>
</div>
<RoomPanel snapshots={data.snapshots} t={t} />
</section>
<section className="panel" id="work">
<div className="panel-title">
<h2>{t.panels.scheduled}</h2>
<span>{t.panels.promptPreviews}</span>
</div>
<TaskPanel locale={locale} tasks={data.tasks} t={t} />
</section>
</>
{activeView === 'scheduled' ? (
<section className="panel view-panel" id="scheduled">
<div className="panel-title">
<h2>{t.panels.scheduled}</h2>
<span>{t.panels.promptPreviews}</span>
</div>
<TaskPanel locale={locale} tasks={data.tasks} t={t} />
</section>
) : null}
</div>
) : null}
</main>
</div>

View File

@@ -70,8 +70,20 @@ table {
.dashboard-content {
display: grid;
align-content: start;
gap: 12px;
min-width: 0;
min-height: calc(100vh - 36px);
}
.view-stack {
display: grid;
gap: 12px;
min-width: 0;
}
.view-panel {
min-height: calc(100vh - 36px);
}
.shell::before {
@@ -248,6 +260,15 @@ button:disabled {
background: rgba(191, 95, 44, 0.12);
}
.side-rail nav a.is-active,
.side-rail nav a[aria-current='page'],
.nav-drawer nav a.is-active,
.nav-drawer nav a[aria-current='page'] {
color: #fffaf0;
background: linear-gradient(135deg, #2b3726, #1c211c);
box-shadow: inset 0 0 0 1px rgba(255, 250, 240, 0.18);
}
.side-refresh {
width: 100%;
min-height: 44px;
@@ -1024,6 +1045,10 @@ progress::-moz-progress-bar {
display: none;
}
.section-nav .menu-button span {
display: block;
}
.section-nav button {
min-width: 0;
color: transparent;