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

+4 -4
View File
@@ -17,12 +17,12 @@ import { GhostBtn, QuietBtn } from '@/shared/components/sentry';
function sanitizeRedirect(raw: string | null): string {
if (!raw) {
return '/projects';
return '/';
}
const value = raw.trim();
if (!value.startsWith('/') || value.startsWith('//')) {
return '/projects';
return '/';
}
return value;
@@ -271,7 +271,7 @@ export function AcceptInvitePage() {
const [isSubmitting, setIsSubmitting] = useState(false);
if (sessionQuery.data) {
return <Navigate to="/projects" replace />;
return <Navigate to="/" replace />;
}
const boundEmail = inviteQuery.data?.email ?? undefined;
@@ -288,7 +288,7 @@ export function AcceptInvitePage() {
// account, sign-in creates the browser session.
await signInWithEmail(effectiveEmail.trim(), password);
await queryClient.invalidateQueries({ queryKey: ['auth-session'] });
navigate('/projects', { replace: true });
navigate('/', { replace: true });
} catch (exception) {
setError(exception instanceof Error ? exception.message : 'Failed to accept invite');
} finally {
@@ -148,6 +148,7 @@ type RangeKey = '1h' | '24h' | '7d';
function formatRelative(date?: string): string {
if (!date) return '—';
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 < 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`;
@@ -376,7 +376,12 @@ export function NodeDetailPage() {
<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>
</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>
@@ -40,7 +40,7 @@ export function SetupWizardPage() {
mutationFn: completeSetup,
onSuccess: () => {
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.
if (status && !status.needs_setup && !override) {
return <Navigate to="/projects" replace />;
return <Navigate to="/" replace />;
}
const steps: { key: Step; label: string }[] = [
@@ -1124,6 +1124,9 @@ export function ProjectWorkspacePage() {
return next;
});
}}
navTargets={viewItems
.filter((item) => signedIn || !item.authOnly)
.map((item) => ({ label: `Go to ${item.label}`, target: item.key }))}
/>
{/* Error Banner */}