fix(ui): deep audit pass — palette navigation, crash fix, honest badges

- CommandPalette: full route navigation, project deep-links fetched on
  open, Create group only where onAddService is wired, dead Connect
  items removed, decorative kbd hints dropped; workspace passes its own
  view targets
- NodeDetailPage crash: container status arrives as a Docker state
  object, not a string — flattened in listAgentContainers and guarded
  at render for persisted-cache payloads
- env badge reflects import.meta.env.MODE instead of hardcoded
  'production'
- Relative times capped: >1y or future timestamps render 'n/a'/'—'
  (zero-time heartbeats no longer show 739898d ago)
- Post-auth and setup redirects land on / (dashboard is the index)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-10-10 17:04:07 +02:00
1 parent 24b9b1267e
commit fc82c30765
9 files changed
+154 -153

No files matched your search

+20 -9
View File
@@ -43,9 +43,11 @@ import {
markAllNotificationsRead, markAllNotificationsRead,
markNotificationRead, markNotificationRead,
pullUpgradeImage, pullUpgradeImage,
listProjects,
} from '@/lib/api-client'; } from '@/lib/api-client';
import { signOutAuthSession } from '@/lib/auth-client'; import { signOutAuthSession } from '@/lib/auth-client';
import { isDemoSearch } from '@/lib/demo-mode'; import { isDemoSearch } from '@/lib/demo-mode';
import { demoProjects } from '@/lib/demo-data';
import { useAuthSession } from '@/lib/use-auth-session'; import { useAuthSession } from '@/lib/use-auth-session';
import { BannerBar, BrandWordmark, CommandPalette, useToast } from '@/shared/components'; import { BannerBar, BrandWordmark, CommandPalette, useToast } from '@/shared/components';
@@ -273,16 +275,20 @@ export function PlatformShell() {
.toUpperCase() || 'C'; .toUpperCase() || 'C';
const paletteNavigate = (target: string) => { const paletteNavigate = (target: string) => {
const routes: Record<string, string> = { navigate(href(target));
dashboard: '/',
canvas: '/projects',
logs: '/activity',
observability: '/operations',
settings: '/settings',
};
navigate(href(routes[target] ?? '/'));
}; };
// Project deep-links for the palette — only fetched once it opens.
const paletteProjectsQuery = useQuery({
queryKey: ['palette-projects'],
queryFn: () => listProjects(),
enabled: paletteOpen && !isDemoMode,
staleTime: 60_000,
});
const paletteProjects = isDemoMode
? demoProjects
: paletteProjectsQuery.data ?? [];
if (!isDemoMode && signedIn && setupQuery.data?.needs_setup) { if (!isDemoMode && signedIn && setupQuery.data?.needs_setup) {
return <Navigate to="/setup" replace />; return <Navigate to="/setup" replace />;
} }
@@ -474,7 +480,7 @@ export function PlatformShell() {
<kbd>⌘ K</kbd> <kbd>⌘ K</kbd>
</button> </button>
<span className="v-env">{isDemoMode ? 'env:demo' : 'env:production'}</span> <span className="v-env">{isDemoMode ? 'env:demo' : `env:${import.meta.env.MODE}`}</span>
<div className="flex-1" /> <div className="flex-1" />
@@ -623,6 +629,11 @@ export function PlatformShell() {
open={paletteOpen} open={paletteOpen}
onClose={() => setPaletteOpen(false)} onClose={() => setPaletteOpen(false)}
onNavigate={paletteNavigate} onNavigate={paletteNavigate}
openTargets={paletteProjects.map((p) => ({
label: p.name,
target: `/projects/${p.id}`,
description: 'Project workspace',
}))}
/> />
</div> </div>
); );
+4 -4
View File
@@ -17,12 +17,12 @@ import { GhostBtn, QuietBtn } from '@/shared/components/sentry';
function sanitizeRedirect(raw: string | null): string { function sanitizeRedirect(raw: string | null): string {
if (!raw) { if (!raw) {
return '/projects'; return '/';
} }
const value = raw.trim(); const value = raw.trim();
if (!value.startsWith('/') || value.startsWith('//')) { if (!value.startsWith('/') || value.startsWith('//')) {
return '/projects'; return '/';
} }
return value; return value;
@@ -271,7 +271,7 @@ export function AcceptInvitePage() {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
if (sessionQuery.data) { if (sessionQuery.data) {
return <Navigate to="/projects" replace />; return <Navigate to="/" replace />;
} }
const boundEmail = inviteQuery.data?.email ?? undefined; const boundEmail = inviteQuery.data?.email ?? undefined;
@@ -288,7 +288,7 @@ export function AcceptInvitePage() {
// account, sign-in creates the browser session. // account, sign-in creates the browser session.
await signInWithEmail(effectiveEmail.trim(), password); await signInWithEmail(effectiveEmail.trim(), password);
await queryClient.invalidateQueries({ queryKey: ['auth-session'] }); await queryClient.invalidateQueries({ queryKey: ['auth-session'] });
navigate('/projects', { replace: true }); navigate('/', { replace: true });
} catch (exception) { } catch (exception) {
setError(exception instanceof Error ? exception.message : 'Failed to accept invite'); setError(exception instanceof Error ? exception.message : 'Failed to accept invite');
} finally { } finally {
@@ -148,6 +148,7 @@ type RangeKey = '1h' | '24h' | '7d';
function formatRelative(date?: string): string { function formatRelative(date?: string): string {
if (!date) return '—'; if (!date) return '—';
const diff = Date.now() - new Date(date).getTime(); const diff = Date.now() - new Date(date).getTime();
if (diff < 0 || diff > 365 * 86_400_000) return '—';
if (diff < 60_000) return 'just now'; if (diff < 60_000) return 'just now';
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`; if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
@@ -376,7 +376,12 @@ export function NodeDetailPage() {
<p className="truncate text-sm text-[var(--text-primary)]">{c.name ?? c.id}</p> <p className="truncate text-sm text-[var(--text-primary)]">{c.name ?? c.id}</p>
<p className="truncate v-mono text-[11px] text-[var(--text-tertiary)]">{c.image}</p> <p className="truncate v-mono text-[11px] text-[var(--text-tertiary)]">{c.image}</p>
</div> </div>
<span className="v-mono text-[11px] text-[var(--text-secondary)]">{c.status ?? '—'}</span> {/* Persisted query cache can replay the raw status object — flatten defensively. */}
<span className="v-mono text-[11px] text-[var(--text-secondary)]">
{typeof c.status === 'string'
? c.status
: (c.status as { state?: string } | undefined)?.state ?? '—'}
</span>
</div> </div>
))} ))}
</div> </div>
@@ -40,7 +40,7 @@ export function SetupWizardPage() {
mutationFn: completeSetup, mutationFn: completeSetup,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['setup-status'] }); queryClient.invalidateQueries({ queryKey: ['setup-status'] });
navigate('/projects', { replace: true }); navigate('/', { replace: true });
}, },
}); });
@@ -54,7 +54,7 @@ export function SetupWizardPage() {
// Wizard is optional once the instance is already configured. // Wizard is optional once the instance is already configured.
if (status && !status.needs_setup && !override) { if (status && !status.needs_setup && !override) {
return <Navigate to="/projects" replace />; return <Navigate to="/" replace />;
} }
const steps: { key: Step; label: string }[] = [ const steps: { key: Step; label: string }[] = [
@@ -1124,6 +1124,9 @@ export function ProjectWorkspacePage() {
return next; return next;
}); });
}} }}
navTargets={viewItems
.filter((item) => signedIn || !item.authOnly)
.map((item) => ({ label: `Go to ${item.label}`, target: item.key }))}
/> />
{/* Error Banner */} {/* Error Banner */}
+10 -2
View File
@@ -1239,9 +1239,17 @@ export type AgentContainer = {
created_at?: string; created_at?: string;
}; };
type RawAgentContainer = Omit<AgentContainer, 'status'> & {
status?: string | { state?: string; health?: string };
};
export async function listAgentContainers(id: string): Promise<AgentContainer[]> { export async function listAgentContainers(id: string): Promise<AgentContainer[]> {
const payload = await requestJson<{ containers?: AgentContainer[] }>(`/agents/${id}/containers`); const payload = await requestJson<{ containers?: RawAgentContainer[] }>(`/agents/${id}/containers`);
return payload.containers ?? []; // The wire sends status as a Docker state object — flatten to its state string.
return (payload.containers ?? []).map((c) => ({
...c,
status: typeof c.status === 'string' ? c.status : c.status?.state || '—',
}));
} }
export type AgentCommand = { export type AgentCommand = {
+5
View File
@@ -13,6 +13,11 @@ export function formatRelative(iso?: string): string {
const hour = 60 * minute; const hour = 60 * minute;
const day = 24 * hour; const day = 24 * hour;
// Future-dated or year-plus-old timestamps are effectively meaningless —
// surface them as absent rather than "739898d ago".
if (diffMs < 0 || diffMs > 365 * day) {
return 'n/a';
}
if (diffMs < minute) { if (diffMs < minute) {
return 'just now'; return 'just now';
} }
@@ -1,24 +1,25 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { import {
ArrowRight,
Box, Box,
CornerDownRight,
Database, Database,
Github,
Image,
Clock, Clock,
Search, Search,
Settings,
FileText,
Activity,
LayoutGrid,
} from 'lucide-react'; } from 'lucide-react';
export interface PaletteTarget {
label: string;
target: string;
description?: string;
}
interface CommandItem { interface CommandItem {
id: string; id: string;
label: string; label: string;
description?: string; description?: string;
icon: typeof Box; icon: typeof Box;
shortcut?: string;
action: () => void; action: () => void;
category?: string; category?: string;
} }
@@ -27,14 +28,39 @@ interface CommandPaletteProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onAddService?: (type: string) => void; onAddService?: (type: string) => void;
onNavigate?: (path: string) => void; onNavigate?: (target: string) => void;
/** Navigate-group items; falls back to the full app route map. */
navTargets?: PaletteTarget[];
/** Extra searchable "Open …" rows, e.g. projects. */
openTargets?: PaletteTarget[];
} }
const APP_NAV: PaletteTarget[] = [
{ label: 'Dashboard', target: '/', description: 'Index — stats, nodes, deploys' },
{ label: 'Projects', target: '/projects', description: 'All projects' },
{ label: 'Templates', target: '/templates', description: 'Template catalog' },
{ label: 'Builds', target: '/builds', description: 'Build pipeline' },
{ label: 'Databases', target: '/databases', description: 'Managed databases' },
{ label: 'Operations', target: '/operations', description: 'Backups, cron, maintenance' },
{ label: 'Activity', target: '/activity', description: 'Platform event feed' },
{ label: 'High Availability', target: '/ha', description: 'Failover & alerts' },
{ label: 'Security', target: '/security', description: 'Access, sessions, secrets' },
{ label: 'Usage', target: '/usage', description: 'Resource consumption' },
{ label: 'People', target: '/people', description: 'Team & access control' },
{ label: 'Audit Logs', target: '/settings/audit-logs', description: 'Recorded actions' },
{ label: 'Webhooks', target: '/settings/webhooks', description: 'Signed event delivery' },
{ label: 'Settings', target: '/settings', description: 'Platform preferences' },
{ label: 'Docs', target: '/docs', description: 'Guides & reference' },
{ label: 'Landing', target: '/landing', description: 'Public page' },
];
export function CommandPalette({ export function CommandPalette({
open, open,
onClose, onClose,
onAddService, onAddService,
onNavigate, onNavigate,
navTargets,
openTargets,
}: CommandPaletteProps) { }: CommandPaletteProps) {
if (!open) return null; if (!open) return null;
@@ -43,6 +69,8 @@ export function CommandPalette({
onClose={onClose} onClose={onClose}
onAddService={onAddService} onAddService={onAddService}
onNavigate={onNavigate} onNavigate={onNavigate}
navTargets={navTargets}
openTargets={openTargets}
/> />
); );
} }
@@ -51,6 +79,8 @@ function CommandPaletteContent({
onClose, onClose,
onAddService, onAddService,
onNavigate, onNavigate,
navTargets,
openTargets,
}: Omit<CommandPaletteProps, 'open'>) { }: Omit<CommandPaletteProps, 'open'>) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@@ -66,130 +96,69 @@ function CommandPaletteContent({
return () => document.removeEventListener('keydown', down); return () => document.removeEventListener('keydown', down);
}, [onClose]); }, [onClose]);
const commands: CommandItem[] = [ const commands: CommandItem[] = [];
{
id: 'add-web', // Create items only exist where onAddService is wired — elsewhere the
label: 'Add Web Service', // entries would silently no-op.
description: 'Deploy a web application', if (onAddService) {
icon: Box, commands.push(
shortcut: 'W', {
category: 'Create', id: 'add-web',
action: () => { label: 'Add Web Service',
onAddService?.('web'); description: 'Deploy a web application',
onClose(); icon: Box,
category: 'Create',
action: () => { onAddService('web'); onClose(); },
}, },
}, {
{ id: 'add-worker',
id: 'add-worker', label: 'Add Worker Service',
label: 'Add Worker Service', description: 'Deploy a background worker',
description: 'Deploy a background worker', icon: Clock,
icon: Clock, category: 'Create',
shortcut: 'K', action: () => { onAddService('worker'); onClose(); },
category: 'Create',
action: () => {
onAddService?.('worker');
onClose();
}, },
}, {
{ id: 'add-database',
id: 'add-database', label: 'Add Database',
label: 'Add Database', description: 'Provision a PostgreSQL database',
description: 'Provision a PostgreSQL database', icon: Database,
icon: Database, category: 'Create',
shortcut: 'D', action: () => { onAddService('database'); onClose(); },
category: 'Create',
action: () => {
onAddService?.('database');
onClose();
}, },
}, {
{ id: 'add-cron',
id: 'add-cron', label: 'Add Cron Job',
label: 'Add Cron Job', description: 'Schedule a recurring task',
description: 'Schedule a recurring task', icon: Clock,
icon: Clock, category: 'Create',
category: 'Create', action: () => { onAddService('cron'); onClose(); },
action: () => {
onAddService?.('cron');
onClose();
}, },
}, );
{ }
id: 'connect-github',
label: 'Connect GitHub Repo', if (onNavigate) {
description: 'Link a repository for auto-deploy', (navTargets ?? APP_NAV).forEach((t, i) =>
icon: Github, commands.push({
category: 'Connect', id: `nav-${i}`,
action: () => { label: t.label,
onClose(); description: t.description,
}, icon: CornerDownRight,
}, category: 'Navigate',
{ action: () => { onNavigate(t.target); onClose(); },
id: 'deploy-image', }),
label: 'Deploy Docker Image', );
description: 'Deploy from a container registry', (openTargets ?? []).forEach((t, i) =>
icon: Image, commands.push({
category: 'Connect', id: `open-${i}`,
action: () => { label: t.label,
onClose(); description: t.description,
}, icon: ArrowRight,
}, category: 'Open',
{ action: () => { onNavigate(t.target); onClose(); },
id: 'goto-dashboard', }),
label: 'Go to Dashboard', );
icon: LayoutGrid, }
shortcut: 'H',
category: 'Navigate',
action: () => {
onNavigate?.('dashboard');
onClose();
},
},
{
id: 'goto-canvas',
label: 'Go to Canvas',
icon: LayoutGrid,
shortcut: 'C',
category: 'Navigate',
action: () => {
onNavigate?.('canvas');
onClose();
},
},
{
id: 'goto-logs',
label: 'Go to Logs',
icon: FileText,
shortcut: 'L',
category: 'Navigate',
action: () => {
onNavigate?.('logs');
onClose();
},
},
{
id: 'goto-metrics',
label: 'Go to Metrics',
icon: Activity,
shortcut: 'M',
category: 'Navigate',
action: () => {
onNavigate?.('observability');
onClose();
},
},
{
id: 'goto-settings',
label: 'Go to Settings',
icon: Settings,
shortcut: 'S',
category: 'Navigate',
action: () => {
onNavigate?.('settings');
onClose();
},
},
];
const filteredCommands = commands.filter( const filteredCommands = commands.filter(
(cmd) => (cmd) =>
@@ -208,6 +177,10 @@ function CommandPaletteContent({
{} as Record<string, CommandItem[]> {} as Record<string, CommandItem[]>
); );
const placeholder = onNavigate
? 'Search pages, projects, actions…'
: 'What would you like to create?';
return ( return (
<div className="fixed inset-0 z-50"> <div className="fixed inset-0 z-50">
<div <div
@@ -224,7 +197,7 @@ function CommandPaletteContent({
<Command.Input <Command.Input
value={search} value={search}
onValueChange={setSearch} onValueChange={setSearch}
placeholder="What would you like to create?" placeholder={placeholder}
className="flex-1 bg-transparent text-[var(--text-primary)] placeholder:text-[var(--text-muted)] text-sm outline-none" className="flex-1 bg-transparent text-[var(--text-primary)] placeholder:text-[var(--text-muted)] text-sm outline-none"
/> />
<kbd className="px-2 py-0.5 rounded bg-[var(--surface-muted)] text-[10px] font-mono text-[var(--text-muted)]"> <kbd className="px-2 py-0.5 rounded bg-[var(--surface-muted)] text-[10px] font-mono text-[var(--text-muted)]">
@@ -251,18 +224,13 @@ function CommandPaletteContent({
onSelect={item.action} onSelect={item.action}
className="flex items-center gap-3 px-3 py-2 rounded-[var(--radius-md)] cursor-pointer text-[var(--text-secondary)] hover:bg-[var(--surface-muted)] hover:text-[var(--text-primary)] aria-selected:bg-[var(--accent-primary-soft)] aria-selected:text-[var(--accent-primary)] transition-colors" className="flex items-center gap-3 px-3 py-2 rounded-[var(--radius-md)] cursor-pointer text-[var(--text-secondary)] hover:bg-[var(--surface-muted)] hover:text-[var(--text-primary)] aria-selected:bg-[var(--accent-primary-soft)] aria-selected:text-[var(--accent-primary)] transition-colors"
> >
<span className="s-ibox !h-7 !w-7"><Icon /></span> <span className="s-ibox !h-7 !w-7"><Icon size={13.5} /></span>
<div className="flex-1"> <div className="flex-1">
<div className="text-sm font-medium">{item.label}</div> <div className="text-sm font-medium">{item.label}</div>
{item.description && ( {item.description && (
<div className="text-xs text-[var(--text-muted)]">{item.description}</div> <div className="text-xs text-[var(--text-muted)]">{item.description}</div>
)} )}
</div> </div>
{item.shortcut && (
<kbd className="px-2 py-0.5 rounded bg-[var(--surface-muted)] text-[10px] font-mono text-[var(--text-muted)]">
{item.shortcut}
</kbd>
)}
</Command.Item> </Command.Item>
); );
})} })}