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
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:
| Range | Minutes | Result |
|---|---|---|
| All of 2027 | 525,600 | zero disagreements — windows, weekday rule, and weekend all match |
| 2026-10-07 → 2027-01-01 | 123,840 | the 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_UTCcarries 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
| File | Role |
|---|---|
index.js | Host half. Intentionally a no-op: nothing here needs the Host, and there is no Host↔Client RPC. |
client.js | The whole feature — the seat, the badge, the dialog, and the schedule. Served to the browser by the client module registry. |
cordis.patch.yml | Inserts the single Loader row that mounts the host half. The client half needs no row. |
test/schedule.test.mjs | Boundary tests for the tariff rule. |
test/format-delay.test.mjs | Hebrew phrasing of the countdown, including the dual and the last minute. |
test/library-crosscheck.test.mjs | Minute-by-minute comparison against deepseek-peak-hours-warning v0.2.0. |
test/dom-seat.test.mjs | Seat, heal, and teardown contract, on a purpose-built fake DOM. |
test/dom-harness.mjs | That fake DOM, plus the loader that evaluates client.js with stubs in scope. |
locale/*.json | Plugin 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.jsandnode --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.