Skip to content

Architecture

Module map (v2)

src/
├── main.tsx                      # entry: mounts <App/>, imports CSS
├── types.ts                      # LogEntry, Filter, Tweaks, DeviceInfo,
│                                 # WidgetKind, Tile, LayoutState

├── lib/
│   ├── filters.ts                # parse/match/highlight — pure
│   ├── logGenerator.ts           # simulator (lazy-loaded)
│   ├── tweaks.ts                 # useTweaks() — localStorage prefs
│   ├── adb.ts                    # WebUSB + ADB transport (lazy)
│   ├── adbContext.ts             # AdbContext + useAdb() hook
│   ├── AdbProvider.tsx           # <AdbProvider/> — wraps the dashboard
│   ├── logStream.ts              # LogStreamHub — single-upstream pub/sub
│   ├── logStreamContext.ts       # context + useLogStream()
│   ├── dashboardChrome.ts        # tweaks + showToast for widgets
│   ├── widgets.ts                # widget registry (kind → def)
│   ├── layout.ts                 # dwindle binary-tree layout + persistence
│   ├── shellSim.ts               # in-memory ADB-shell built-ins (sim path)
│   ├── format.ts                 # formatTs, rowHeightFor
│   └── knownNames.ts             # static tag/process pools for autocomplete

├── components/
│   ├── App.tsx                   # owns device + LogStreamHub; renders
│   │                             # <EmptyState/> or <Dashboard/>
│   ├── EmptyState.tsx            # pre-connection screen
│   ├── Dashboard.tsx             # connected shell + DashTopbar
│   ├── TileGrid.tsx              # dwindle tree renderer; split-handle resize, swap-drag
│   ├── Tile.tsx                  # tile chrome (header + body)
│   ├── WidgetPalette.tsx         # +Add widget modal
│   ├── FilterBar.tsx             # chip input + transport
│   ├── LevelRow.tsx              # V/D/I/W/E pills + rate
│   ├── LogList.tsx               # virtualised log area + sticky pinned block
│   ├── LogRow.tsx                # one log line, with highlight rendering
│   ├── Heatmap.tsx               # 60-cell gutter
│   ├── HelpDialog.tsx            # ? shortcuts dialog
│   ├── SearchOverlay.tsx         # ⌘F floating search box
│   ├── Icons.tsx                 # inline SVG icon set
│   └── widgets/
│       ├── LogcatWidget.tsx      # the v1 logcat experience as a widget
│       ├── ShellWidget.tsx       # interactive ADB shell (one channel per instance)
│       ├── DumpsysWidget.tsx     # preset dumpsys runner + parsed cards / raw view
│       ├── FilesWidget.tsx       # tree + list browser over adb.sync()
│       └── MirrorWidget.tsx      # scrcpy-style mirror with WebCodecs decode

└── styles/
    ├── tokens.css                # design original — colors, spacing, motion
    │                             # (extended with v2 --shadow-3 + --glass-line)
    ├── app.css                   # design original — layout + log row + panels
    ├── dashboard.css             # design original (v2) — tile grid + chrome
    ├── components.css            # cross-cutting additions (empty state, filter
    │                             # bar, level pills, heatmap, help dialog,
    │                             # palette, tile loading)
    └── widgets/                  # per-widget design CSS (Phase 10 split);
        ├── logcat.css            # imported by the widget itself so the
        ├── shell.css             # rules co-load with the lazy chunk
        ├── dumpsys.css
        ├── files.css
        └── mirror.css

tokens.css, app.css, and dashboard.css are design originals — copied verbatim from design/v1/source/styles.css (tokens + app) and design/v2/source/dashboard.css. The v1 → v2 design diff added two tokens (--shadow-3 + --glass-line); they're added inline at the top of each theme block in tokens.css. Everything else stays untouched so those files can be refreshed from the design source without merge conflicts. Per-widget CSS lives under styles/widgets/<kind>.css and is imported at the top of the matching widget component. Cross-cutting additions (palette, tile loading, help dialog, heatmap, etc.) stay in components.css.

State

Top-level state lives in <App/>:

StateTypeNotes
deviceDeviceInfo | nullnull ⇒ render <EmptyState/>
usingFakebooleantrue when streaming the simulator
adbAdb | nulllive ADB handle (Phase 6+)
tweaksTweaks (via useTweaks)persisted prefs
toaststring | nullbottom-centre acknowledgement
helpOpenboolean? shortcuts dialog

Widget state — filters, paused, autoScroll, levelEnabled, pinned, expanded — moved from <App/> to <LogcatWidget/>. Two Logcat tiles on the same device get independent chip bars (persisted under weblogcat:filters:<serial>:<tileId>).

Tile-grid state lives in <TileGrid/>: the layout tree, the maximized tile id, and the in-flight resize / swap-drag state. Persisted to localStorage under weblogcat-dashboard-v2. The tree is a binary "dwindle" layout (Hyprland-style) — each split node carves its parent area along one axis at a configurable ratio; leaves host tiles. Adding a tile splits the focused leaf in two; removing collapses the parent split into the surviving sibling. Resizing happens at the seam between two siblings (.dash-split-handle); rearrangement is drag-to-swap on tile headers. The dashboard is a fixed-size, non-scrolling viewport — tiles share every pixel of available space.

The stream

<App/> owns a single LogStreamHub — see lib/logStream.ts. The transport (real ADB or simulator) calls hub.publishMany(entries) once per ingest tick; the hub keeps a ring buffer (cap MAX_LOGS = 5000) and fans out to N LogcatWidget subscribers. New subscribers get a snapshot on mount.

Why a hub: K Logcat tiles previously meant K shell channels and K × 50k row buffers. With the hub there's one upstream and one buffer; per- widget memory is just whatever the widget keeps for its filter view.

Performance notes

  • <LogRow/> is wrapped in memo; <LogList/> virtualises past 800 visible rows via @tanstack/react-virtual.
  • The resize / swap-drag loop in <TileGrid/> coalesces pointer-move events into the next animation frame — protects against >120Hz pointer devices flooding React with state updates.

Theming

tokens.css defines the entire palette as CSS custom properties keyed off --accent-hue (set by data-accent) and theme (data-theme). The useTweaks hook applies both attributes to <html> so changes propagate without any JS-side recompute.

Released under the MIT license.