librqbit 9.0.1

The main library used by rqbit torrent client. The binary is just a small wrapper on top of it.
Documentation
# WebUI Architecture Guide

This document helps Claude work efficiently on the rqbit webui codebase.

## Tech Stack
- React 18 + TypeScript
- Tailwind CSS (dark mode via `dark:` prefix)
- Zustand for state management
- Vite for dev/build
- react-icons for icons (BsX, FaX, MdX, GoX prefixes)

## Directory Structure
```
src/
├── api-types.ts        # TypeScript types matching backend API
├── http-api.ts         # API client (all backend calls)
├── context.tsx         # React contexts (APIContext)
├── rqbit-web.tsx       # App shell, header, menu buttons
├── main.tsx            # Entry point
├── stores/             # Zustand stores
│   ├── torrentStore.ts # Global torrent list, loading states
│   ├── uiStore.ts      # View mode, selection state
│   ├── errorStore.ts   # Alerts and errors
│   └── statsStore.ts   # Session-wide stats
├── hooks/              # Custom React hooks
├── helper/             # Utility functions (formatBytes, etc.)
└── components/
    ├── RootContent.tsx         # Main content area, layout switching
    ├── CardLayout.tsx          # Card view layout (list of cards)
    ├── TorrentCard.tsx         # Card view data wrapper per torrent
    ├── TorrentCardContent.tsx  # Card view single torrent content
    ├── compact/                # Compact/table view components
    │   ├── CompactLayout.tsx    # Table view layout
    │   ├── TorrentTable.tsx     # Table with headers
    │   ├── TorrentTableRow.tsx  # Single table row
    │   ├── ActionBar.tsx        # Bulk action buttons
    │   ├── DetailPane.tsx       # Bottom detail panel
    │   └── *Tab.tsx             # Detail tabs
    ├── buttons/           # Reusable buttons
    ├── modal/             # Modal dialogs
    └── forms/             # Form components
```

## Key Patterns

### Data Fetching (per-torrent)
Each torrent fetches its own data independently. Pattern from `TorrentCard.tsx`:
```typescript
// Details: fetch once, retry on error
useEffect(() => {
  return loopUntilSuccess(async () => {
    await API.getTorrentDetails(id).then(setDetails);
  }, 1000);
}, [forceRefresh]);

// Stats: continuous polling with adaptive interval
useEffect(() => {
  return customSetInterval(async () => {
    return API.getTorrentStats(id).then(stats => {
      setStats(stats);
      // Fast poll (1s) when live, slow (10s) when paused
      return stats.state === "live" ? 1000 : 10000;
    });
  }, 0);
}, [forceRefresh]);
```

### State Management
Zustand stores are simple - just use hooks:
```typescript
// Reading state
const viewMode = useUIStore(state => state.viewMode);
const torrents = useTorrentStore(state => state.torrents);

// Actions
const selectTorrent = useUIStore(state => state.selectTorrent);
selectTorrent(id);
```

### Responsive Design
- Breakpoint: `lg` (1024px) for compact vs card view
- Use `useIsLargeScreen()` hook for JS logic
- Use Tailwind classes for CSS: `lg:flex-row`, `hidden lg:block`

### Dark Mode
Always add dark variants: `bg-white dark:bg-slate-800`

## API Types (api-types.ts)
Key types to know:
- `TorrentId`: `{ id: number, info_hash: string }`
- `TorrentDetails`: `{ name, info_hash, files[] }`
- `TorrentStats`: `{ state, error, progress_bytes, total_bytes, finished, live? }`
- `LiveTorrentStats`: speeds, ETA, peer_stats (only when state="live")

States: `"initializing"`, `"live"`, `"paused"`, `"error"`

## API Methods (http-api.ts)
```typescript
API.listTorrents()           // GET /torrents
API.getTorrentDetails(id)    // GET /torrents/{id}
API.getTorrentStats(id)      // GET /torrents/{id}/stats/v1
API.start(id)                // POST /torrents/{id}/start
API.pause(id)                // POST /torrents/{id}/pause
API.forget(id)               // POST /torrents/{id}/forget (remove from list)
API.delete(id)               // POST /torrents/{id}/delete (remove + delete files)
API.updateOnlyFiles(id, fileIds[])  // POST /torrents/{id}/update_only_files
```

## Adding New Features

### New Component
1. Create in appropriate directory (`components/` or `components/compact/`)
2. Use existing patterns for data fetching if needed
3. Import from parent component

### New Store State
1. Add to existing store or create new in `stores/`
2. Follow Zustand pattern: `create<StoreType>((set, get) => ({...}))`

### New API Call
1. Add type to `api-types.ts`
2. Add method to `http-api.ts` using `makeRequest()`

## Common Tasks

### Show loading state
```typescript
if (!data) return <Spinner />;
```

### Show error
```typescript
const setCloseableError = useErrorStore(state => state.setCloseableError);
setCloseableError({ text: "Error message", details: error });
```

### Refresh torrent data
```typescript
const refreshTorrents = useTorrentStore(state => state.refreshTorrents);
refreshTorrents();
```

### Format display values
```typescript
import { formatBytes } from "../helper/formatBytes";
import { getCompletionETA } from "../helper/getCompletionETA";
import { torrentDisplayName } from "../helper/getTorrentDisplayName";
```

## Testing Changes
```bash
# Dev server (hot reload)
npm run dev  # or: make webui-dev from repo root

# Type check
npx tsc --noEmit

# Format code (run from repo root)
npm run format

# Build
npm run build
```

**Always run `npm run format` from the repo root after modifying TypeScript/TSX files.**

Dev server runs at http://localhost:3031/, connects to backend at :3030.

## Mock Mode (No Backend Required)

For UI testing without a real rqbit server, use mock mode:

```bash
npm run dev:mock
```

This starts the dev server on port 3032 and opens http://localhost:3032/mock.html with:
- 1000 generated torrents (Linux distro names)
- ~30 active (live/initializing), rest paused
- Simulated download progress for live torrents
- Stable peer IPs with incrementing counters (for speed calculations)
- Working pause/start/forget/delete actions

Mock code (`mock-api.ts`, `main-mock.tsx`, `mock.html`) is excluded from production builds.

Use this to test UI performance, layout with many torrents, or develop without running the full stack.

## Performance Guidelines

When working with large lists (1000+ torrents), follow these patterns:

### Virtualization
Both card and table views use `react-virtuoso` for virtualization - only visible items are rendered to the DOM. See `architecture/virtualization.md` for full details.

**Key requirements:**
- Parent container chain must have explicit height (use `h-full`, `flex-1 min-h-0`)
- Must filter array before passing to Virtuoso (can't use CSS hidden)

**Benefits:**
- Variable height items work automatically (no fixed `itemSize` needed)
- No `AutoSizer` wrapper required
- Simpler API: just `totalCount` and `itemContent` props
- DOM stays small (~500 elements vs 44,000), initial render is 8x faster

### Memoization
- Use `memo()` for row/card components that receive torrent data
- Use `useMemo()` for expensive computations (sorting, filtering for navigation)
- Use `useCallback()` for handlers passed to child components

### Debouncing
- Always debounce search input (150ms is good)
- Use local state for instant feedback, debounced update to store:
```typescript
const [localSearch, setLocalSearch] = useState(searchQuery);
const debouncedSetSearch = useCallback(
  debounce((value: string) => setSearchQuery(value), 150),
  [setSearchQuery]
);
```

### Shared Utilities
Common filtering/sorting logic is in `helper/torrentFilters.ts`:
- `isTorrentVisible(t, query, statusFilter)` - combined visibility check
- `compareTorrents(a, b, column, direction)` - sorting comparison
- Type definitions: `TorrentSortColumn`, `SortDirection`, `StatusFilter`

## Code Style

### Avoid Repetitive CSS Classes
When the same Tailwind class combination appears 3+ times, extract it into a variable:

```typescript
// BAD - repetitive, hard to maintain
<td className="w-20 px-2 text-right text-secondary whitespace-nowrap align-middle">...</td>
<td className="w-20 px-2 text-right text-secondary whitespace-nowrap align-middle">...</td>
<td className="w-20 px-2 text-right text-secondary whitespace-nowrap align-middle">...</td>

// GOOD - extracted into variable
const numericCell = "w-20 px-2 text-right text-secondary whitespace-nowrap align-middle";
<td className={numericCell}>...</td>
<td className={numericCell}>...</td>
<td className={numericCell}>...</td>
```

For more complex cases, consider a small component or use template literal composition:
```typescript
const cellBase = "px-2 align-middle";
const numericCell = `w-20 ${cellBase} text-right text-secondary whitespace-nowrap`;
```