feat(dashboard): lazy routes, Needs-you strip, URL state, exact numbers

- Workspace, SettingsHub, SessionDetail and the Workflow sub-tab are lazy
  (main bundle 2,166 kB -> ~920 kB), wrapped in RouteErrorBoundary: a stale
  chunk after a self-update reloads once (vite:preloadError too), any other
  error shows a Reload panel instead of a blank page.
- Live refresh: lib/throttle.ts + hooks/useLiveRefresh.ts replace trailing
  debounces that froze under a steady event stream; refresh skips hidden
  tabs and refetches after a WebSocket reconnect. Monitor loads only while
  shown, with one batched GET /api/agents?session_ids= instead of N+1; a
  failing endpoint is an inline error, not a blank Dashboard.
- NeedsYouStrip on Monitor and above Analytics. Tabs, filters, page, sort
  and search live in the URL (Dashboard, Sessions, Activity, SessionDetail).
- Sessions: tool / workspace / date-range filters, CSV/JSON export
  (formula-neutralised), focusable row links, Open in workspace (the dead
  /run links are gone), stale responses dropped, error + Retry.
- Fixes: "NaNs" durations, 30/7-day charts skipping idle days, weekday
  labels west of UTC (now averages), cache hit rate denominator, billable
  tokens vs cache reads, exact tool-call counts, "To" date = end of day,
  invalid ?from= no longer throws, recent list no longer shrinks to 15,
  stable message keys, transcript switch races.
- Light-theme-unreadable bg-black code panels and dark-only DAG fills use
  tokens; status configs and chart colours use theme tokens; Health tab
  and other literals translated.
- Server (additive): GET /api/agents?session_ids=, GET /api/sessions
  awaiting=1&from=&to=, stats tool_call_count/distinct_tool_count, event
  rows carry session/agent labels.
This commit is contained in:
2026-10-05 13:22:04 +07:00
parent 8fb703c6db
commit 03f1673fce
63 changed files with 3879 additions and 1662 deletions
+59 -17
View File
@@ -43,6 +43,7 @@
* ----------------------------------------------------------------------------- */
import React, { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Calendar, Clock, ChevronLeft, ChevronRight, X } from "lucide-react";
interface DateTimePickerProps {
@@ -52,16 +53,22 @@ interface DateTimePickerProps {
className?: string;
"aria-label"?: string;
title?: string;
/** A "to" bound: picking a day with no time yet means the END of that day
* (23:59), not its first minute. */
endOfDay?: boolean;
}
export function DateTimePicker({
value,
onChange,
placeholder = "Select date & time",
placeholder,
className = "",
"aria-label": ariaLabel,
title,
endOfDay = false,
}: DateTimePickerProps) {
const { t, i18n } = useTranslation("sessions");
const locale = i18n.resolvedLanguage ?? i18n.language;
const [isOpen, setIsOpen] = useState(false);
const [alignment, setAlignment] = useState<"left" | "right">("left");
const containerRef = useRef<HTMLDivElement>(null);
@@ -97,9 +104,18 @@ export function DateTimePicker({
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
useEffect(() => {
if (!isOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setIsOpen(false);
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [isOpen]);
const formatDisplay = (d: Date | null) => {
if (!d || isNaN(d.getTime())) return "";
return d.toLocaleString(undefined, {
return d.toLocaleString(locale, {
month: "short",
day: "numeric",
hour: "2-digit",
@@ -112,7 +128,9 @@ export function DateTimePicker({
if (dateObj && !isNaN(dateObj.getTime())) {
newDate.setHours(dateObj.getHours(), dateObj.getMinutes());
} else {
newDate.setHours(0, 0); // Default midnight
// Default: midnight, or 23:59 for an end bound so the day is included.
if (endOfDay) newDate.setHours(23, 59, 59, 999);
else newDate.setHours(0, 0);
}
updateValue(newDate);
};
@@ -135,7 +153,9 @@ export function DateTimePicker({
const day = String(d.getDate()).padStart(2, "0");
const h = String(d.getHours()).padStart(2, "0");
const mi = String(d.getMinutes()).padStart(2, "0");
onChange(`${y}-${mo}-${day}T${h}:${mi}`);
// An end bound carries its :59.999 so the whole last minute is included.
const tail = endOfDay ? ":59.999" : "";
onChange(`${y}-${mo}-${day}T${h}:${mi}${tail}`);
};
const clearValue = (e: React.MouseEvent) => {
@@ -168,10 +188,15 @@ export function DateTimePicker({
key={i}
type="button"
onClick={() => handleDateClick(i)}
aria-label={new Date(viewDate.getFullYear(), viewDate.getMonth(), i).toLocaleDateString(
locale,
{ weekday: "long", year: "numeric", month: "long", day: "numeric" }
)}
aria-pressed={isSelected}
className={`w-6 h-6 flex items-center justify-center rounded text-[11px] transition-colors
${
isSelected
? "bg-accent text-white font-medium"
? "bg-accent-solid text-white font-medium"
: isToday
? "bg-surface-3 text-accent font-medium"
: "hover:bg-surface-2 text-fg-secondary hover:text-fg-primary"
@@ -193,20 +218,27 @@ export function DateTimePicker({
type="button"
onClick={() => setIsOpen(!isOpen)}
aria-label={ariaLabel}
aria-expanded={isOpen}
aria-haspopup="dialog"
title={title}
className={`flex items-center gap-2 bg-surface-2 border ${isOpen ? "border-accent" : "border-border"} rounded px-2 py-1.5 min-w-[150px] text-xs focus:outline-none focus:border-accent transition-colors w-full text-left`}
className={`flex items-center gap-2 bg-surface-2 border ${isOpen ? "border-accent" : "border-border"} rounded px-2 py-1.5 min-w-[150px] text-xs focus:outline-none focus:border-accent transition-colors w-full text-left ${dateObj ? "pr-7" : ""}`}
>
<Calendar className="w-3.5 h-3.5 text-fg-secondary shrink-0" />
<span className={`flex-1 truncate ${!dateObj ? "text-fg-muted" : "text-fg-secondary"}`}>
{dateObj ? formatDisplay(dateObj) : placeholder}
{dateObj ? formatDisplay(dateObj) : (placeholder ?? t("datePicker.placeholder"))}
</span>
{dateObj && (
<X
className="w-3 h-3 text-fg-muted hover:text-fg-primary shrink-0"
onClick={clearValue}
/>
)}
</button>
{/* A sibling, not a child: a button inside a button is not reachable by keyboard. */}
{dateObj && (
<button
type="button"
onClick={clearValue}
aria-label={t("datePicker.clear")}
className="absolute right-1.5 top-1/2 -translate-y-1/2 p-0.5 rounded text-fg-muted hover:text-fg-primary"
>
<X className="w-3 h-3" />
</button>
)}
{isOpen && (
<div
@@ -219,18 +251,20 @@ export function DateTimePicker({
onClick={() =>
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1))
}
aria-label={t("datePicker.prevMonth")}
className="p-1 hover:bg-surface-2 rounded text-fg-secondary hover:text-fg-primary"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-xs font-medium text-fg-secondary">
{viewDate.toLocaleString(undefined, { month: "long", year: "numeric" })}
{viewDate.toLocaleString(locale, { month: "long", year: "numeric" })}
</span>
<button
type="button"
onClick={() =>
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1))
}
aria-label={t("datePicker.nextMonth")}
className="p-1 hover:bg-surface-2 rounded text-fg-secondary hover:text-fg-primary"
>
<ChevronRight className="w-4 h-4" />
@@ -240,8 +274,15 @@ export function DateTimePicker({
{/* Calendar Grid */}
<div>
<div className="grid grid-cols-7 gap-1 mb-1">
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((day) => (
<div key={day} className="w-6 text-center text-[10px] font-medium text-fg-muted">
{/* 2026-01-04 is a Sunday: seven local days give the weekday initials. */}
{Array.from({ length: 7 }, (_, d) =>
new Date(2026, 0, 4 + d).toLocaleDateString(locale, { weekday: "narrow" })
).map((day, d) => (
<div
key={d}
aria-hidden
className="w-6 text-center text-[10px] font-medium text-fg-muted"
>
{day}
</div>
))}
@@ -253,10 +294,11 @@ export function DateTimePicker({
<div className="pt-3 border-t border-border flex items-center justify-between">
<div className="flex items-center gap-1.5 text-fg-secondary">
<Clock className="w-3.5 h-3.5" />
<span className="text-[11px] font-medium">Time</span>
<span className="text-[11px] font-medium">{t("datePicker.time")}</span>
</div>
<input
type="time"
aria-label={t("datePicker.time")}
value={timeValue}
onChange={handleTimeChange}
className="bg-surface-2 border border-border rounded px-2 py-1 text-xs text-fg-secondary focus:outline-none focus:border-accent w-[85px] time-input-custom"