~bigbes/lethe

3fbbfc8996f7197100b9780478d9810f480a510e — Eugene Blikh a month ago f6611d7
web: review fixes for projects/stats SPA routes

- ProjectsTable: drop the inner navigate call from handleOpen;
  the parent route already navigates via the onOpen callback,
  so the second push was creating a duplicate history entry
  on every row click. Matches the SessionsTable pattern.

- HorizontalBars: replace href:string with onActivate(row)
  callback. The earlier shape passed a pre-encoded path
  string straight into TanStack <Link to={...}>; routing the
  navigation through the typed (to, params) form via the
  caller avoids any double-encoding ambiguity around splat
  params and decouples the primitive from a specific route.

- stats.css: drop duplicated .card / .card-head / .card-body
  blocks. The same rules already live in shell.css (loaded
  globally), so any future divergence between the two copies
  would silently desync.
M internal/server/web/dist/index.html => internal/server/web/dist/index.html +2 -2
@@ 10,8 10,8 @@
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap"
      rel="stylesheet"
    />
    <script type="module" crossorigin src="/assets/index-C1oZtiYo.js"></script>
    <link rel="stylesheet" crossorigin href="/assets/index-C5WzPe--.css">
    <script type="module" crossorigin src="/assets/index-Ch2vbLAm.js"></script>
    <link rel="stylesheet" crossorigin href="/assets/index-BjmMFw4W.css">
  </head>
  <body class="density-compact">
    <div id="root"></div>

M web/src/features/projects/ProjectsTable.tsx => web/src/features/projects/ProjectsTable.tsx +1 -9
@@ 1,5 1,4 @@
import React from 'react'
import { useNavigate } from '@tanstack/react-router'
import { EmptyState, ToolDot, Spark } from '../../primitives'
import type { Project } from '../../api/adapters'



@@ 39,8 38,6 @@ function formatLastActive(iso: string): string {
}

export function ProjectsTable({ projects, cursor, onCursor, onOpen }: ProjectsTableProps): React.JSX.Element {
  const navigate = useNavigate()

  if (projects.length === 0) {
    return (
      <div className="body body-pad">


@@ 49,11 46,6 @@ export function ProjectsTable({ projects, cursor, onCursor, onOpen }: ProjectsTa
    )
  }

  function handleOpen(p: Project) {
    onOpen(p)
    void navigate({ to: '/project/$', params: { _splat: encodeURIComponent(p.cwd) } })
  }

  const maxSessions = Math.max(...projects.map(p => p.sessions), 1)

  return (


@@ 75,7 67,7 @@ export function ProjectsTable({ projects, cursor, onCursor, onOpen }: ProjectsTa
            key={p.cwd}
            className={'projects-row projects-cols' + (isCursor ? ' cursor' : '')}
            style={{ gridTemplateColumns: COLS }}
            onClick={() => { onCursor(i); handleOpen(p) }}
            onClick={() => { onCursor(i); onOpen(p) }}
            onMouseEnter={() => onCursor(i)}
          >
            <span className="mono truncate">{p.cwd}</span>

M web/src/primitives/HorizontalBars.tsx => web/src/primitives/HorizontalBars.tsx +21 -12
@@ 1,34 1,43 @@
import type React from 'react'
import { Link } from '@tanstack/react-router'

interface HorizontalBarsRow {
  label: string
  count: number
  href?: string
}

interface HorizontalBarsProps {
  rows: HorizontalBarsRow[]
interface HorizontalBarsProps<R extends HorizontalBarsRow = HorizontalBarsRow> {
  rows: R[]
  max: number
  onActivate?: (row: R) => void
}

export function HorizontalBars({ rows, max }: HorizontalBarsProps): React.JSX.Element {
export function HorizontalBars<R extends HorizontalBarsRow>(
  { rows, max, onActivate }: HorizontalBarsProps<R>,
): React.JSX.Element {
  const safeMax = max > 0 ? max : 1

  return (
    <div>
      {rows.map((row, i) => {
        const ratio = row.count / safeMax
        const label = row.href != null ? (
          <Link
            to={row.href}
        const activatable = onActivate != null
        const labelStyle: React.CSSProperties = {
          fontSize: 11,
          ...(activatable
            ? { color: 'var(--accent-ink)', cursor: 'pointer' }
            : {}),
        }
        const label = (
          <span
            className="mono truncate"
            style={{ fontSize: 11, color: 'var(--accent-ink)', textDecoration: 'none' }}
            style={labelStyle}
            onClick={activatable ? () => onActivate(row) : undefined}
            role={activatable ? 'button' : undefined}
            tabIndex={activatable ? 0 : undefined}
            onKeyDown={activatable ? (e) => { if (e.key === 'Enter') onActivate(row) } : undefined}
          >
            {row.label}
          </Link>
        ) : (
          <span className="mono truncate" style={{ fontSize: 11 }}>{row.label}</span>
          </span>
        )

        return (

M web/src/routes/stats.tsx => web/src/routes/stats.tsx +8 -2
@@ 245,7 245,7 @@ function StatsRoute(): React.JSX.Element {
  const topCwdRows = s.topCwd.map(r => ({
    label: r.cwd,
    count: r.count,
    href: `/project/${encodeURIComponent(r.cwd)}`,
    cwd: r.cwd,
  }))
  const topCwdCard = (
    <div className="card">


@@ 254,7 254,13 @@ function StatsRoute(): React.JSX.Element {
        {s.topCwd.length === 0 ? (
          <EmptyState glyph="∅" copy="no cwd data in this range" />
        ) : (
          <HorizontalBars rows={topCwdRows} max={topCwdMax} />
          <HorizontalBars
            rows={topCwdRows}
            max={topCwdMax}
            onActivate={(row) => {
              void navigate({ to: '/project/$', params: { _splat: encodeURIComponent(row.cwd) } })
            }}
          />
        )}
      </div>
    </div>

M web/src/styles/stats.css => web/src/styles/stats.css +1 -23
@@ 12,29 12,7 @@
  grid-column: 1 / -1;
}

/* Card base (mirrors prototype.css) */
.card {
  border: 1px solid var(--rule-2);
  border-radius: 4px;
  background: var(--paper-4);
}

.card .card-head {
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-3);
  padding: 8px 10px 6px;
  border-bottom: 1px solid var(--rule-2);
  display: flex;
  align-items: center;
  gap: 8px;
}

.card .card-body {
  padding: 10px;
}
/* .card / .card-head / .card-body live in shell.css (loaded globally). */

/* Per-tool rollup rows */
.per-tool-row {