# dsh-usage-dashboard

**English** | [中文](./README.md)

A usage dashboard plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): a balance pill in the bottom-right corner of the Web UI that expands into a full panel showing your **actual billed spend** (same data source as platform.deepseek.com/usage) — no more switching back to the platform to check costs.

> **Scope:** This plugin queries only balance, usage, and billing data from the official DeepSeek API and DeepSeek Platform. Even if Harness is configured with another model provider, the plugin does not read that provider's billing data. The panel may continue to show DeepSeek data, show an unavailable state, or report an error; none of these represent the current model provider's actual balance or cost.

The current stable version is `1.1.1`. See [CHANGELOG_EN.md](./CHANGELOG_EN.md) for the changes in every version.

## Preview

After installation and configuration, the bottom-right balance pill expands into the full DeepSeek usage dashboard (peak/valley banner, today/this-month metrics, usage heatmap, and model donut):

![DeepSeek Harness usage dashboard (dark theme, peak hour, English)](./docs/images/usage-dashboard-panel-en-dark-peak.jpg)

## Screenshots

**Panel** — dark / light × Chinese / English:

<table>
  <tr>
    <td align="center"><img src="./docs/images/usage-dashboard-panel-zh-dark-peak.jpg" width="420" alt="Dark theme, peak hour, Chinese panel"><br><sub>Dark · peak · 中文</sub></td>
    <td align="center"><img src="./docs/images/usage-dashboard-panel-en-dark-peak.jpg" width="420" alt="Dark theme, peak hour, English panel"><br><sub>Dark · peak · English</sub></td>
  </tr>
  <tr>
    <td align="center"><img src="./docs/images/usage-dashboard-panel-zh-light-valley.jpg" width="420" alt="Light theme, valley hour, Chinese panel"><br><sub>Light · valley · 中文</sub></td>
    <td align="center"><img src="./docs/images/usage-dashboard-panel-en-light-valley.jpg" width="420" alt="Light theme, valley hour, English panel"><br><sub>Light · valley · English</sub></td>
  </tr>
</table>

**Balance pill** — amber breathing glow at peak, green steady glow at valley:

<table>
  <tr>
    <td align="center"><img src="./docs/images/usage-dashboard-pill-peak.jpg" width="360" alt="Peak-hour pill with amber breathing glow"><br><sub>Peak · amber breathing glow</sub></td>
    <td align="center"><img src="./docs/images/usage-dashboard-pill-valley.jpg" width="360" alt="Valley-hour pill with green steady glow"><br><sub>Valley · green steady glow</sub></td>
  </tr>
</table>


## Features

- **Balance pill**: a compact capsule in the bottom-right corner showing the live balance; during **peak hours** its border glows amber with a soft pulse, during valley hours it glows green; click to open the panel, and click anywhere outside (or the pill again) to close — both open and close animate with a 160 ms fade
- **Account balance**: official `/user/balance` (API key) + platform `get_user_summary` (userToken), with top-up vs. granted breakdown; amounts follow the account currency (`¥`/`$`/`€`) — symbol only, **no FX conversion**
- **Metric cards (3×2 grid)**: Today amount · Today tokens · Requests (day) / Month amount · Month tokens · Cache hit (month); each today card carries a “share of month” progress bar
- **Usage heatmap** (GitHub style, collapsed by default): click the “Usage heatmap” heading to expand; daily amount/tokens over the last `historyMonths` months (default 6), darker = higher; hovering a cell pops a live tooltip card with that day's amount, tokens, requests and cache-hit rate (days without usage say “no usage this day”). The expanded state is remembered in the browser
- **Model donut**: grouped by **day / week / month**, browsable with **‹ ›** between periods, and sorted **by amount / by tokens**; the legend shows **both dimensions at once** (the primary value follows the sort dimension, the other rides along as small text on the same row); the center shows the period total; small models are folded into a gray “Other” (a model is listed individually when its share is ≥1.5% and within the top 8)
- **Peak/valley banner**: a unified banner at the top of the panel (amber peak / green valley with a compact countdown pill) giving live guidance; default peak windows `09:00–12:00` / `14:00–18:00` Beijing time, other hours are valley at ~50% off; windows configurable via `peakWindows`
- **In-panel update hint**: when npm has a newer version the footer version becomes a clickable badge (showing `current → available`) that copies the upgrade command (configured via `updateCommand`); it never auto-installs, and a `dsh web` restart is required afterwards
- **Chinese/English + in-panel language switch**: Settings offers “Follow interface / 中文 / English”; the default follows the Harness language, an explicit choice affects this panel only and is stored in the browser (other languages fall back to English through the locale fallback chain)
- **userToken panel**: paste once, online validation, one-click clear, masked display

## Refresh strategy

| Trigger | Description |
|---|---|
| Fixed polling | Host fetches DeepSeek every `refreshIntervalMs` (default 10 min); browser reads the local cache every `clientPollIntervalMs` (default 30 s); polling pauses while the page is hidden and resumes on foreground |
| Task-completion refresh | Listens for session `turn/end` events and fetches DeepSeek once right after each task, with a minimum cooldown of `taskRefreshCooldownMs` (default 60 s) to coalesce bursts |
| Manual | The ↻ button force-refreshes through the cache; opening the panel, switching chart dimensions, and saving settings also refresh immediately |

> DeepSeek billing settles with a few minutes of delay, so numbers fetched right after a task may not be fully settled yet; the next polling cycle catches up automatically.

## DeepSeek data sources

| Data | Endpoint | Credential |
|---|---|---|
| Official balance | `GET {apiBaseUrl}/user/balance` | API key (default `DEEPSEEK_API_KEY`, resolved via `ctx.credentials`) |
| Platform balance | `GET {platformBaseUrl}/api/v0/users/get_user_summary` | platform `userToken` |
| Daily usage | `GET {platformBaseUrl}/api/v0/usage/by_api_key/amount?start=&end=&tz=` | platform `userToken` |
| Daily cost | `GET {platformBaseUrl}/api/v0/usage/by_api_key/cost?start=&end=&tz=` | platform `userToken` |
| Daily usage (fallback) | `GET {platformBaseUrl}/api/v0/usage/amount?month=&year=` | platform `userToken` |
| Daily cost (fallback) | `GET {platformBaseUrl}/api/v0/usage/cost?month=&year=` | platform `userToken` |

> `start`/`end` are epoch seconds and `tz` is the timezone offset in seconds (`timezoneOffsetSec`, default `28800` = GMT+8).
> These are the endpoints the official usage page itself calls: buckets are cut by GMT+8 and the **current day updates live**
> (the legacy endpoint cuts days by UTC and always reports the current day as zero, so it is only a fallback).

> The platform usage endpoints are **undocumented** (the same ones the usage page calls, and already used by community apps). They may break without notice if the platform changes; the plugin parses defensively, keeps the last successful data on failure, and never affects Harness itself.

## ⚠️ Security & privacy (read before use)

- **userToken is an account-level credential.** Billed data comes from platform.deepseek.com's signed-in endpoints and requires your `userToken` (equivalent to your platform account's access credential). Treat it like a password and **never** paste it into public channels, chats, or any git repository.
- **Traffic goes only to official DeepSeek domains**: the token is used solely for **read-only** queries against three undocumented usage endpoints on `platform.deepseek.com`, plus the official `api.deepseek.com/user/balance`. The plugin contains no telemetry, analytics, or third-party forwarding.
- **Local storage**: the token is stored in `$DSH_HOME/storages/dsh-usage-dashboard.secret` (mode 0600, readable only by the host process). The browser only ever receives the masked value (`abcd****wxyz`); the plaintext token is never sent to the page. You can also use the `DEEPSEEK_PLATFORM_TOKEN` environment variable instead.
- **This repository and its code contain no credentials**; the plugin never prints, logs, or uploads the token.
- **Sanitize installation records before sharing them**: never run or share commands that print the complete `.credentials.yaml`, environment, or token values. Before sending logs or a session archive, search for and remove `sk-`, `userToken`, `DEEPSEEK_API_KEY`, and `DEEPSEEK_PLATFORM_TOKEN`. Delete unneeded diagnostic snapshots such as `$DSH_HOME/logs/dsh-web-env-snapshot.json`.
- **Undocumented endpoint risk**: `/api/v0/usage/*` are internal platform endpoints with no SLA; they may change without notice (this never affects Harness itself, and failures keep the last good data).
- **How to remove**: panel ⚙️ settings → "Clear saved token", or delete `~/.dsh/storages/dsh-usage-dashboard.secret`.
- By using this plugin you acknowledge the risks above and assume them yourself. See [SECURITY.md](./SECURITY.md) for details.

## Installation

### Prerequisites

- Node.js `18` or newer
- A working DeepSeek Harness installation
- `pnpm` available (the plugin installer invokes it)

Check first:

```sh
node --version
pnpm --version
```

If the second command is not found, install the pinned version:

```sh
npm install --global pnpm@10.15.0
```

### Option 1: npm package (recommended)

No repository clone is required. Whether future upgrades are one command away depends on the version spec you install with — choose per your needs:

#### Option 1A: track updates (default recommendation, easiest upgrades)

```sh
dsh plugin --profile web add deepseek-harness-usage-dashboard
```

Without a fixed version, pnpm records a range such as `^1.1.1`. Later, a single command tracks the newest 1.x:

```sh
dsh plugin --profile web update deepseek-harness-usage-dashboard
```

#### Option 1B: pinned version (reproducible)

```sh
dsh plugin --profile web add deepseek-harness-usage-dashboard@1.1.1
```

The explicit `1.1.1` keeps the installation reproducible when later versions are released. To upgrade, re-run the command with the new version number.

### Option 2: GitHub Release `.tgz` (when npm is unavailable or pnpm integrity policy blocks a URL)

Download the `.tgz` from the [v1.1.1 Release](https://github.com/nzz0991999-ai/dsh-usage-dashboard/releases/tag/v1.1.1), then install it with a local `file:` path. This lets pnpm record the tarball in the Profile lockfile:

```powershell
dsh plugin --profile web add "file:C:/Users/your-name/Downloads/deepseek-harness-usage-dashboard-1.1.1.tgz"
Get-FileHash "C:/Users/your-name/Downloads/deepseek-harness-usage-dashboard-1.1.1.tgz" -Algorithm SHA256
```

The SHA-256 must match the value published on the Release page. You may also try the remote URL directly:

```sh
dsh plugin --profile web add https://github.com/nzz0991999-ai/dsh-usage-dashboard/releases/download/v1.1.1/deepseek-harness-usage-dashboard-1.1.1.tgz
```

### Configure DeepSeek Platform userToken

`DEEPSEEK_API_KEY` and `userToken` are different credentials. The API key serves the official balance endpoint; the platform sign-in `userToken` serves daily/monthly usage and actual billed-spend endpoints. `platform.deepseek.com` and `api.deepseek.com` are DeepSeek's official shared domains, not relays operated by this plugin.

1. Sign in to <https://platform.deepseek.com> with Chrome or Edge and keep the page signed in.
2. Press `F12` → **Application** → **Local Storage** → `https://platform.deepseek.com`.
3. Search for `userToken` and copy only its **Value**, without the key name, quotes, or surrounding whitespace. If it is missing, refresh or sign in again and recheck.
4. In Harness, open the bottom-right dashboard → ⚙️ settings → paste → "Verify & save". Only a masked value is shown after saving.

Never paste a `userToken` into a terminal, chat, Issue, screenshot, or installation log. Advanced users may set `DEEPSEEK_PLATFORM_TOKEN` before starting Harness, but shell history can retain plaintext, so the dashboard form is safer.

## Upgrading

When a newer version is available, run the command for your installation method, then restart `dsh web` from the same workspace directory and hard-refresh the page:

| Installation | Upgrade to latest |
|---|---|
| npm track-updates (Option 1A) | `dsh plugin --profile web update deepseek-harness-usage-dashboard` |
| npm pinned (Option 1B) | `dsh plugin --profile web add deepseek-harness-usage-dashboard@<new-version>` |
| GitHub Release `.tgz` (Option 2) | Download the new `.tgz` from the Release, then `dsh plugin --profile web add "file:…"` again |

Check for updates first:

```sh
dsh plugin --profile web outdated
```

> With Option 1A (track updates), `update` moves you to the newest 1.x automatically; with Option 1B (pinned), `update` never jumps versions — change the spec to the new version explicitly. That is the reproducibility trade-off.

### In-panel update hint

When npm has a newer version, the footer version becomes a clickable amber badge showing `current → available`:

<table>
  <tr>
    <td align="center"><img src="./docs/images/usage-dashboard-update-panel.jpg" width="150" alt="Panel when an update is available: the footer version becomes a badge"><br><sub>① footer version becomes a badge</sub></td>
    <td align="center"><img src="./docs/images/usage-dashboard-update-badge.png" width="230" alt="Update badge: current → available"><br><sub>② badge: current → available</sub></td>
    <td align="center"><img src="./docs/images/usage-dashboard-update-copied.png" width="230" alt="After clicking: the upgrade command is copied"><br><sub>③ after clicking: command copied</sub></td>
  </tr>
</table>

**What to do after clicking** — four steps, and step 3 is not optional:

1. **Click the badge**: the upgrade command goes to the clipboard and the badge turns green with “✓ Upgrade command copied” (back to normal after ~2.5s); hovering previews the command. The badge **only copies text — it runs nothing and touches no local file**.
2. **Paste it into your terminal**: the command comes from the `updateCommand` config, by default `dsh plugin --profile web update deepseek-harness-usage-dashboard`.
3. **Restart `dsh web`**: the host loads plugin code at boot, so replacing files on disk alone has no effect.
4. **Hard-refresh the page** (Cmd/Ctrl+Shift+R): the client bundle is served at boot too. After the restart the badge disappears (the local version is now the latest).

> The hint only appears when you open the panel — it never pops up on its own. `updateCheckIntervalMs` controls how often it checks (6 h by default) and `checkUpdate: false` turns it off entirely.

## Installation troubleshooting

### `EADDRINUSE: address already in use 127.0.0.1:3080`

This normally means that another `dsh web` is already running; it is not a plugin, API-key, or `userToken` failure. If the existing page opens, use and refresh it instead of starting a duplicate server.

In Windows PowerShell, identify the listener first:

```powershell
$dshPid = Get-NetTCPConnection -LocalPort 3080 -State Listen |
  Select-Object -First 1 -ExpandProperty OwningProcess
Get-CimInstance Win32_Process -Filter "ProcessId=$dshPid" |
  Select-Object ProcessId, CommandLine
```

Only after confirming it is the old `dsh web` process, stop it and restart from the correct workspace:

```powershell
taskkill /PID $dshPid /T /F
Set-Location "F:/path/to/your/harness-workspace"
dsh web
```

On macOS/Linux, use `lsof -nP -iTCP:3080 -sTCP:LISTEN`, confirm the process, run `kill <PID>`, and start again from the original workspace.

### `ERR_MODULE_NOT_FOUND: @deepseek-ai/schemastery`

A local directory installed as a `link:` dependency may omit dependencies. Remove the old plugin and reinstall from the npm package or the Release `.tgz`:

```sh
dsh plugin --profile web remove dsh-usage-dashboard
dsh plugin --profile web add deepseek-harness-usage-dashboard@1.1.1
```

### No balance pill after installation

Confirm both installation and restart use the `web` profile and start Harness from the original workspace, then hard-refresh the browser page. If an old `dsh web` process is still running, identify and restart it using the port steps above.

## Configuration overrides

Write into `$DSH_HOME/profiles/web/cordis.patch.yml`:

```yaml
- id: dsh-usage-dashboard
  config:
    refreshIntervalMs: 300000      # how often the host fetches DeepSeek (ms)
    clientPollIntervalMs: 15000    # how often the browser reads the cache (ms)
    timeoutMs: 8000                # per-request timeout (ms)
    historyMonths: 6               # months browsable in the panel
    apiKeyRef: DEEPSEEK_API_KEY    # credential reference for the official balance
    timezoneOffsetSec: 28800       # usage/cost timezone offset (seconds), default GMT+8; decides "today" and day buckets, ignores the browser timezone
    taskRefreshCooldownMs: 60000   # min cooldown of the task-completion refresh (ms)
    peakWindows: [09:00-12:00, 14:00-18:00]   # peak windows (HH:MM-HH:MM, Beijing time)
    checkUpdate: true               # check npm for a newer version (hint only, never auto-installs)
    updateCheckIntervalMs: 21600000 # how often to check for updates (ms), default 6 h
    updateCommand: dsh plugin --profile web update deepseek-harness-usage-dashboard  # command copied by the footer badge
```

## Release process (maintainers)

Write and commit the section for the target version in **both changelogs**
(`CHANGELOG.md` in Chinese and `CHANGELOG_EN.md` in English) first — that section is
the single source for the GitHub Release body. Then one command does the rest:

```bash
npm run release -- 1.3.0 "short summary"
```

`scripts/release.sh` performs, in order:

1. **Preflight**: on `main`, in sync with `origin/main`, clean tree, `gh` logged in, version increases, tag unused, both changelogs contain the version section
2. **Local checks**: `npm test` + `node --check client/client.js` + `verify:package`
3. **Commit**: bump `package.json` → commit `release: vX.Y.Z <summary>` → push
4. **Tag**: annotated tag `vX.Y.Z` → push
5. **Release**: compose the body from the changelog sections (Chinese + collapsed English + compare link) → `gh release create --verify-tag`
6. **npm hint**: with `--publish` it also runs `npm publish --access public` (an account requiring 2FA needs an interactive terminal for the OTP)

| Flag | Effect |
|---|---|
| `--dry-run` | Validate and preview the plan and the release body, **changing nothing** |
| `--yes` | Skip the confirmation prompt (scripted/CI use) |
| `--skip-checks` | Skip the local checks in step 2 |
| `--publish` | Actually run `npm publish` in step 6 |
| `--branch <name>` | Release from a branch other than `main` |

> The script never does anything destructive: no history rewrite, no `--force`, no automatic changelog commits. A non-increasing version, an existing tag, or a dirty tree aborts immediately.

## Uninstall

```sh
dsh plugin --profile web remove deepseek-harness-usage-dashboard
```

For `1.0.0` or older installations that used the old package name, run `dsh plugin --profile web remove dsh-usage-dashboard` instead. If you no longer need it, also delete `$DSH_HOME/storages/dsh-usage-dashboard.secret`.
