Skip to content
Merged
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
10 changes: 7 additions & 3 deletions dashboard/src/v2/ErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { useRef, useEffect } from 'preact/hooks';
import { Link } from '@tanstack/react-router';
import { gsap } from 'gsap';
import { CanvasBackground } from './components/CanvasBackground.js';
import { useOptionalDashboardI18n } from './i18n/context.js';
import { shellMessages } from './i18n/messages/shell.js';

interface ErrorPageProps {
errorCode?: number;
message?: string;
}

export function ErrorPage({ errorCode = 404, message = 'Page not found' }: ErrorPageProps) {
export function ErrorPage({ errorCode = 404, message }: ErrorPageProps) {
const { translate } = useOptionalDashboardI18n();
const resolvedMessage = message ?? translate(shellMessages, "pageNotFound");
const contentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -32,13 +36,13 @@ export function ErrorPage({ errorCode = 404, message = 'Page not found' }: Error
{errorCode}
</h1>
<p className="text-xl text-slate-400 dark:text-slate-400 mt-4 mb-8">
{message}
{resolvedMessage}
</p>
<Link
to="/"
className="bg-[#00E0A0] text-[#0d0f12] font-semibold px-6 py-3 rounded-[1.25rem] hover:opacity-90 transition-opacity"
>
Go Home
{translate(shellMessages, "goHome")}
</Link>
</div>
</div>
Expand Down
32 changes: 20 additions & 12 deletions dashboard/src/v2/components/DockerStatusMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { Box, CircleAlert, Play, Square, Terminal } from "lucide-preact";
import { useFocusTrap } from "../hooks/use-focus-trap";
import { fetchOnboardingReadiness } from "../../lib/api/dashboard-api.js";
import type { OnboardingRuntimeReadiness } from "../../types.js";
import { useOptionalDashboardI18n } from "../i18n/context.js";
import { shellMessages } from "../i18n/messages/shell.js";

export interface DockerContainer {
id: string;
Expand All @@ -24,6 +26,7 @@ export interface DockerContainer {
}

export const DockerStatusMenu: FunctionComponent = () => {
const { formatNumber, translate } = useOptionalDashboardI18n();
const [containers, setContainers] = useState<DockerContainer[]>([]);
const [readiness, setReadiness] = useState<OnboardingRuntimeReadiness | null>(null);
const [loading, setLoading] = useState(false);
Expand Down Expand Up @@ -127,9 +130,12 @@ export const DockerStatusMenu: FunctionComponent = () => {

const activeContainers = containers.filter(c => c.state === "running");
const clusterNotReady = readiness?.cluster.status === "not_ready";
const dockerStatusLabel = clusterNotReady
? `Docker Status: runtime not ready, ${activeContainers.length} active containers`
: `Docker Status: ${activeContainers.length} active containers`;
const activeCount = formatNumber(activeContainers.length);
const dockerStatusLabel = translate(
shellMessages,
clusterNotReady ? "dockerStatusNotReady" : "dockerStatus",
{ count: activeCount },
);

return (
<div className="relative inline-flex items-center gap-2" ref={containerRef}>
Expand All @@ -153,7 +159,7 @@ export const DockerStatusMenu: FunctionComponent = () => {
!
</span>
</span>
Runtime not ready
{translate(shellMessages, "runtimeNotReady")}
</div>
) : null}
<div
Expand Down Expand Up @@ -222,34 +228,36 @@ export const DockerStatusMenu: FunctionComponent = () => {
role="dialog"
id={menuId}
aria-modal="true"
aria-label="Active Docker Containers"
aria-label={translate(shellMessages, "activeDockerContainers")}
className="fixed inset-x-4 top-[72px] md:inset-auto md:absolute md:top-full md:right-0 mt-2 w-80 max-w-[calc(100vw-2rem)] max-h-[calc(100dvh-5rem)] bg-white/95 dark:bg-void-800/95 backdrop-blur-2xl border border-black/[0.06] dark:border-white/[0.08] rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.12)] dark:shadow-[0_20px_40px_rgba(0,0,0,0.4)] overflow-hidden z-50 flex flex-col"
>
<div className="px-4 py-3 border-b border-black/[0.04] dark:border-white/[0.04] flex items-center justify-between shrink-0">
<span className="text-xs font-bold uppercase tracking-[0.1em] text-slate-800 dark:text-slate-200">
Docker Containers
{translate(shellMessages, "dockerContainers")}
</span>
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-black/[0.03] dark:bg-white/[0.03]">
<span className={`w-1.5 h-1.5 rounded-full ${clusterNotReady ? "bg-status-red motion-safe:animate-pulse motion-reduce:animate-none" : "bg-signal-500 motion-safe:animate-pulse"}`} />
<span className={`text-[10px] font-mono font-medium ${clusterNotReady ? "text-status-red" : "text-slate-500 dark:text-slate-400"}`}>
{clusterNotReady ? "Not Ready" : `${activeContainers.length} Active`}
{clusterNotReady
? translate(shellMessages, "notReady")
: translate(shellMessages, "activeCount", { count: activeCount })}
</span>
</div>
</div>

<div className="flex-1 min-h-0 overflow-y-auto overscroll-contain">
{loading ? (
<div className="flex items-center justify-center py-8">
<span className="text-xs font-medium text-slate-400">Loading containers...</span>
<span className="text-xs font-medium text-slate-400">{translate(shellMessages, "loadingContainers")}</span>
</div>
) : clusterNotReady ? (
<div className="flex flex-col gap-3 px-4 py-5">
<div className="flex items-start gap-3 rounded-xl border border-status-red/25 bg-status-red/10 p-3">
<CircleAlert className="mt-0.5 h-4 w-4 shrink-0 text-status-red" strokeWidth={2.4} aria-hidden="true" />
<div>
<div className="text-sm font-bold text-slate-800 dark:text-slate-100">Docker is mandatory</div>
<div className="text-sm font-bold text-slate-800 dark:text-slate-100">{translate(shellMessages, "dockerMandatory")}</div>
<div className="mt-1 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
Docker is not running or not reachable. Code UX runs provider CLIs inside containers, so task execution cannot start until Docker is available.
{translate(shellMessages, "dockerMandatoryHelp")}
</div>
</div>
</div>
Expand All @@ -270,8 +278,8 @@ export const DockerStatusMenu: FunctionComponent = () => {
) : containers.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 px-4 text-center">
<Box className="w-8 h-8 text-slate-300 dark:text-slate-600 mb-2" strokeWidth={1.5} />
<span className="text-sm font-medium text-slate-600 dark:text-slate-300">No Containers</span>
<span className="text-xs text-slate-400 mt-1">Docker is not running any containers.</span>
<span className="text-sm font-medium text-slate-600 dark:text-slate-300">{translate(shellMessages, "noContainers")}</span>
<span className="text-xs text-slate-400 mt-1">{translate(shellMessages, "noContainersHelp")}</span>
</div>
) : (
<div className="flex flex-col p-1.5 gap-1.5">
Expand Down
11 changes: 8 additions & 3 deletions dashboard/src/v2/components/KineticDock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
isRouteNavigationItem,
type PrimaryNavigationItem,
} from "../lib/navigation-items.js";
import { useOptionalDashboardI18n } from "../i18n/context.js";
import { shellMessages } from "../i18n/messages/shell.js";

type DockItem = PrimaryNavigationItem;

Expand Down Expand Up @@ -43,6 +45,7 @@ interface KineticDockProps {
}

export const KineticDock: FunctionComponent<KineticDockProps> = ({ experienceMode }) => {
const { locale, translate } = useOptionalDashboardI18n();
const dockRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLAnchorElement | null)[]>([]);
const [indicatorState, setIndicatorState] = useState({ left: 22, initialized: false });
Expand Down Expand Up @@ -194,7 +197,7 @@ export const KineticDock: FunctionComponent<KineticDockProps> = ({ experienceMod

const renderItem = (item: DockItem, globalIndex: number) => {
const isActive = activeIndex === globalIndex;
const label = getNavigationItemLabel(item, "dock");
const label = getNavigationItemLabel(item, "dock", locale);
const className = `relative group flex flex-col items-center justify-center w-[52px] h-[52px] min-w-[44px] min-h-[44px] shrink-0 snap-center rounded-[1.4rem] transition-[background-color,border-color,box-shadow] motion-reduce:transition-none duration-300 decoration-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal-500 focus-visible:ring-offset-2 focus-visible:ring-offset-[#F9F8F4] dark:focus-visible:ring-offset-void-800 ${
isActive
? "bg-signal-500/[0.12] shadow-[inset_0_0_0_1px_rgba(0,224,160,0.32),0_10px_24px_rgba(0,224,160,0.12)]"
Expand Down Expand Up @@ -272,7 +275,7 @@ export const KineticDock: FunctionComponent<KineticDockProps> = ({ experienceMod
className="fixed bottom-0 left-0 right-0 z-50 flex justify-center items-end pointer-events-none px-4 max-w-[100vw] overflow-hidden"
>
<nav
aria-label="Dock navigation"
aria-label={translate(shellMessages, "dockNavigation")}
aria-describedby="dock-route-status"
ref={dockRef}
onPointerMove={handlePointerMove}
Expand Down Expand Up @@ -310,7 +313,9 @@ export const KineticDock: FunctionComponent<KineticDockProps> = ({ experienceMod
{/* Right edge scroll spacer */}
<div className="w-[1px] shrink-0" aria-hidden="true" />
<span id="dock-route-status" role="status" aria-live="polite" className="sr-only">
Active route: {activeItem ? getNavigationItemLabel(activeItem, "dock") : "None"}
{translate(shellMessages, "activeRoute", {
label: activeItem ? getNavigationItemLabel(activeItem, "dock", locale) : translate(shellMessages, "none"),
})}
</span>
</nav>
</div>
Expand Down
49 changes: 30 additions & 19 deletions dashboard/src/v2/components/NotificationDetailsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@ import { useMemo, useRef } from "preact/hooks";
import { ArrowRight, Clock3, X } from "lucide-preact";
import type { DashboardNotification } from "../hooks/use-notifications.js";
import { Modal } from "./ui/Modal.js";
import { useOptionalDashboardI18n } from "../i18n/context.js";
import { shellMessages } from "../i18n/messages/shell.js";
import type { NotificationDetail } from "../lib/notification-view-models.js";

const formatDetailValue = (label: string, value: string): string => {
if (label !== "Timestamp") return value;
const detailMessageKey = {
project: "notificationProject", sprint: "notificationSprint", task: "notificationTask",
summary: "notificationWhatWentWrong", reason: "notificationWhyAttention",
instructions: "notificationRecommendedSteps", timestamp: "notificationTimestamp",
source: "notificationSourceContext",
} as const satisfies Record<NonNullable<NotificationDetail["kind"]>, string>;

const isTimestampDetail = (detail: NotificationDetail): boolean => detail.kind === "timestamp" || detail.label === "Timestamp";
const isWideDetail = (detail: NotificationDetail): boolean => (
detail.kind === "summary" || detail.kind === "reason" || detail.kind === "instructions" || detail.kind === "source"
|| detail.label === "What went wrong" || detail.label === "Why this needs attention"
|| detail.label === "Recommended next steps" || detail.label === "Source context"
);

const parseDetailTimestamp = (value: string): Date | null => {
const timestamp = new Date(value);
if (!Number.isFinite(timestamp.getTime())) return value;
return new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short",
}).format(timestamp);
return Number.isFinite(timestamp.getTime()) ? timestamp : null;
};

export const NotificationDetailsModal: FunctionComponent<{
Expand All @@ -20,6 +32,7 @@ export const NotificationDetailsModal: FunctionComponent<{
onAction: (notification: DashboardNotification) => void;
onNavigate?: (href: string) => void | Promise<void>;
}> = ({ notification, onClose, onAction, onNavigate }) => {
const { formatDate, translate } = useOptionalDashboardI18n();
const closeButtonRef = useRef<HTMLButtonElement>(null);
const titleId = notification ? `notification-details-title-${notification.id}` : "notification-details-title";
const descriptionId = notification ? `notification-details-description-${notification.id}` : "notification-details-description";
Expand All @@ -39,21 +52,21 @@ export const NotificationDetailsModal: FunctionComponent<{
<header className="flex shrink-0 items-start justify-between gap-4 border-b border-black/[0.06] px-5 py-4 dark:border-white/[0.06] sm:px-6 sm:py-5">
<div className="min-w-0">
<div className="text-[10px] font-black uppercase tracking-[0.16em] text-signal-700 dark:text-signal-300">
Attention details
{translate(shellMessages, "attentionDetails")}
</div>
<h2 id={titleId} className="mt-1 break-words text-xl font-black tracking-tight text-slate-900 dark:text-white">
{notification.title}
</h2>
<p id={descriptionId} className="mt-2 text-sm leading-relaxed text-slate-500 dark:text-slate-400">
Review the execution context and recommended recovery path.
{translate(shellMessages, "attentionDetailsHelp")}
</p>
</div>
<button
ref={closeButtonRef}
type="button"
onClick={onClose}
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl text-slate-400 transition-colors motion-reduce:transition-none hover:bg-black/[0.05] hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal-500/50 dark:hover:bg-white/[0.06] dark:hover:text-white"
aria-label="Close notification details"
aria-label={translate(shellMessages, "closeNotificationDetails")}
>
<X className="h-4 w-4" aria-hidden="true" />
</button>
Expand All @@ -62,25 +75,23 @@ export const NotificationDetailsModal: FunctionComponent<{
<div className="dashboard-scrollbar min-h-0 flex-1 overflow-y-auto px-5 py-5 sm:px-6">
<dl className="grid gap-3 sm:grid-cols-2">
{details.map((detail) => {
const wide = detail.label === "What went wrong"
|| detail.label === "Why this needs attention"
|| detail.label === "Recommended next steps"
|| detail.label === "Source context";
const wide = isWideDetail(detail);
const timestamp = isTimestampDetail(detail) ? parseDetailTimestamp(detail.value) : null;
return (
<div
key={detail.label}
className={`rounded-2xl border border-black/[0.06] bg-black/[0.025] p-4 dark:border-white/[0.06] dark:bg-white/[0.035] ${wide ? "sm:col-span-2" : ""}`}
>
<dt className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">
{detail.label}
{detail.kind ? translate(shellMessages, detailMessageKey[detail.kind]) : detail.label}
</dt>
<dd className="mt-1.5 break-words text-sm font-medium leading-relaxed text-slate-700 dark:text-slate-200">
{detail.label === "Timestamp" ? (
{isTimestampDetail(detail) ? (
<time dateTime={detail.value} className="inline-flex items-center gap-1.5">
<Clock3 className="h-3.5 w-3.5 text-slate-400" aria-hidden="true" />
{formatDetailValue(detail.label, detail.value)}
{timestamp ? formatDate(timestamp, { dateStyle: "medium", timeStyle: "short" }) : detail.value}
</time>
) : formatDetailValue(detail.label, detail.value)}
) : detail.value}
</dd>
</div>
);
Expand All @@ -94,7 +105,7 @@ export const NotificationDetailsModal: FunctionComponent<{
onClick={onClose}
className="min-h-11 rounded-xl border border-black/10 px-4 text-sm font-bold text-slate-600 transition-colors motion-reduce:transition-none hover:bg-black/[0.04] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-signal-500/50 dark:border-white/10 dark:text-slate-300 dark:hover:bg-white/[0.05]"
>
Close
{translate(shellMessages, "close")}
</button>
{notification.actionHref && notification.actionLabel ? (
<a
Expand Down
Loading
Loading