← Back to home@adesbusy

dsh-peak-indicator

Peak / off-peak pricing badge for the DeepSeek Harness Web GUI: a dot beside the brand wordmark, with the next switch in local time, UTC and a countdown on hover.

Stars
0
Language
JavaScript
Created
Oct 7, 2026
Updated
Oct 7, 2026
GitHub repo

Introduction

dsh-peak-indicator

A DeepSeek peak / off-peak badge for the Harness Web GUI, and the dialog it opens on hover.

… 🐟 ⊙ DeepSeek Harness      ← the dot sits between the brand mark and the wordmark
        amber = peak · green = off-peak

Hovering the dot — or focusing it with the keyboard — opens a card with the tariff in force, the next switch in local time, in UTC and as a countdown ("בעוד שעתיים ו־30 דקות"), and the standing weekly rule.

The schedule it applies

Straight from the DeepSeek pricing page: 01:00–04:00 and 06:00–10:00 UTC, Monday to Friday, excluding Chinese public holidays. Every other hour is off-peak, including weekends and Chinese public holidays in full.

Two consequences worth knowing, because they surprise people:

  • The windows are stated in UTC, not in the machine's local time. In Israel (UTC+3 in summer) that is 04:00–07:00 and 09:00–13:00 local.
  • Peak pricing applies on weekdays only, so a Saturday morning inside the published window is off-peak.

The dialog prints the next switch twice — once in the viewer's local zone, once in UTC — so the answer can be checked against the published window directly.

Cross-checked against an independent implementation

test/library-crosscheck.test.mjs transcribes the rules of deepseek-peak-hours-warning v0.2.0 (MIT), a separate project that bills pi sessions at DeepSeek's peak and off-peak rates, and compares the two implementations minute by minute:

RangeMinutesResult
All of 2027525,600zero disagreements — windows, weekday rule, and weekend all match
2026-10-07 → 2027-01-01123,840the only 420 differing minutes are inside Chinese public holidays

That last row is the deliberate difference: the library does not model Chinese public holidays, so during a holiday week it bills peak where this plugin bills off-peak. DeepSeek's published rule excludes holidays, so this plugin follows the pricing page and the difference is asserted to stay exactly there — a disagreement anywhere else fails the suite.

Two further findings from the library are now recorded in client.js rather than left implicit:

  • The weekend rule started 2026-08-23 00:00 Beijing (2026-08-22 16:00 UTC). WEEKEND_RULE_START_UTC carries that instant, which the library publishes too.
  • The weekend is the same set of instants on either axis. This plugin decides it on the UTC day and the library on the Beijing day; a UTC day is Saturday exactly when Beijing is eight hours into its own Saturday, so the two agree everywhere — which the 2027 sweep demonstrates.

The library's rates were not adopted: it still prices deepseek-v4-flash and deepseek-v4-pro at 2026-08-31 figures ($0.44/$1.32 peak for Flash), while the pricing page now lists deepseek-flash at $0.30/$1.20. This plugin shows the state, not the price, so only the timing rules were taken.

How it takes its place, and why not a slot

The badge is inserted as the first child of the wordmark's flex container (.brandName, the span ui-sidebar renders around the sidebar.brand.name slot output), which is what puts it left of the wordmark with the row's own 6px gap. The badge adds 2px of margin, so the visual gap is 8px — the same gap the sidebar itself uses between the brand mark and the wordmark.

It is not a slot registration, and that is a constraint rather than a preference. sidebar.brand.name is a single slot: one cell, one occupant. SlotCore.register throws single slot "…" already has a registration … when the requested priority is taken, and dsh-client-ui-brand-official already holds the default priority 0. Registering at another priority does not add a sibling either — entriesOfSlot keeps exactly one winner per cell, so a higher priority would shadow the wordmark instead of sitting beside it. There is no additive sibling seat in the brand row.

Because that seat lives inside React-owned markup, a re-render can remove it. A MutationObserver re-inserts it, keyed on a data-dsh-peak-indicator attribute, so a pass is idempotent and a dropped badge heals itself; when the whole row is replaced, the badge is torn down and re-seated on the new one. Nothing here needs React — hand-built DOM cannot mismatch a React version.

The dialog does not flicker

The card element is built once and reused, and a tick rewrites only its textContent. Rebuilding it instead — replacing the node to change a sentence — replays its entry animation once a second, which a pointer resting on a 12px dot sees as strobing. Closing is likewise debounced by CLOSE_DELAY_MS (160ms), so the cursor crossing the dot's edges does not remove and re-insert the card in a loop. test/dom-seat.test.mjs asserts both: the node survives three ticks unreplaced, and a leave followed by a re-enter reuses the same node.

Installation

The package is a bundle: dsh.bundle.patch names the cordis.patch.yml that inserts its single Loader row, which is what makes dsh plugin add work for it.

# from GitHub
dsh plugin add github:adesbusy/dsh-peak-indicator

# or from a local checkout
dsh plugin add "C:\path\to\dsh-peak-indicator"

The plugin manager performs the same installation through its install_bundle action, and the DSH plugin market lists it for one-click install. In every case the profile's package.json and patch layer are written by that command, never by hand.

A page refresh is required the first time the bundle is installed: the open page's module graph is composed at boot, and this environment's page does not pick the new entry up on its own.

Files

FileRole
index.jsHost half. Intentionally a no-op: nothing here needs the Host, and there is no Host↔Client RPC.
client.jsThe whole feature — the seat, the badge, the dialog, and the schedule. Served to the browser by the client module registry.
cordis.patch.ymlInserts the single Loader row that mounts the host half. The client half needs no row.
test/schedule.test.mjsBoundary tests for the tariff rule.
test/format-delay.test.mjsHebrew phrasing of the countdown, including the dual and the last minute.
test/library-crosscheck.test.mjsMinute-by-minute comparison against deepseek-peak-hours-warning v0.2.0.
test/dom-seat.test.mjsSeat, heal, and teardown contract, on a purpose-built fake DOM.
test/dom-harness.mjsThat fake DOM, plus the loader that evaluates client.js with stubs in scope.
locale/*.jsonPlugin Manager card text.

Every suite reads client.js itself — the schedule and phrasing suites slice the regions marked // >>> schedule and // >>> delay out of the bundle, and the seat suite evaluates the whole file against the harness — so none of them can drift from what ships.

npm test

Maintaining the holiday list

CN_HOLIDAYS in client.js carries the Chinese public holidays, keyed by the date in China (UTC+8), for 2026 — the ranges in 国办发明电〔2025〕7号. The 2027 notice is expected in November 2026. Until its ranges are added, the badge falls back to the weekday rule for 2027, which means it will claim peak pricing during a 2027 holiday week rather than off-peak pricing during a working week — the safer of the two errors.

Adding a holiday means extending the list and adding a boundary case to test/schedule.test.mjs, so the next edit has something to fail against.

Verification

  • node --check client.js and node --check index.js — both halves parse.
  • npm test — four suites: 18 tariff boundary assertions (both ends of each window, the weekend, the National Day and Spring Festival ranges, the holiday-boundary UTC midnight that is not a window edge, and a year without a holiday table); 21 countdown phrasings plus the guarantee that no numeral 1 ever sits beside a plural noun; the seat contract (first-child position, hover/focus dialog, a live countdown that does not rebuild the card, debounced close, click containment, heal after a re-render, idempotent passes, heal on a replaced row, and full teardown); and 649,440 minutes compared against the independent library.
  • The bundle patch is parsed with the same js-yaml the loader uses, and the row id/name are checked against the manifest.
  • What is not verified here: the rendered pixels. No browser control is available in this environment, so placement, color, and the card's appearance are established by the source, the fake-DOM contract, and the shipped sidebar markup — not by a screenshot.