Skip to content

Add @magpie-community/page-usage-board (page: full usage board) - #844

Open
orinsid wants to merge 5 commits into
magpie-community:mainfrom
orinsid:page-usage-board
Open

orinsid wants to merge 5 commits into
magpie-community:mainfrom
orinsid:page-usage-board

Conversation

@orinsid

@orinsid orinsid commented Nov 7, 2026 •

Copy link
Copy Markdown

Adds @magpie-community/page-usage-board (page, community: true).

Summary (verbatim from registry.json / package.json magpie.summary)

en: A quota card wall with expiry tiers, daily/per-provider charts, 24h cache-hit and token curves, optional remote attribution and account detail. Runs its own zero-dependency local service reading this machine's Magpie usage data; optional remote attribution queries the configured remote magpie.

zh: 额度到期卡片墙 + 每日与 provider 用量图表 + 24 小时缓存命中与 token 曲线,可选远端归因与账号明细。自带零依赖本机服务,读本机 Magpie 用量数据;远端归因会查询所配置的远端 magpie。

Differences from existing packages

  • vs usage (Usage glance): that page renders today/7d/30d calls, errors, tokens and cost from usage:summary; this one adds the quota wall with expiry tiers, the 7×24 heatmap, recent-request detail and per-account remote attribution. Complements it rather than replacing it.
  • vs page-usage-tree: that page is the provider → account → model drill-down; this one is the one-screen board (quota wall + charts + curves). Complements it.
  • No overlap with the auth/middleware packages: the page reads nothing through the bridge (permissions: []).

What it reads (disclosure)

  • The page itself reads nothing through the bridge (permissions: [], zero magpie.read).
  • The bundled host-part service reads the machine's usage.jsonl (no prompts or message bodies) and magpie's local usage/quota API.
  • Remote attribution is opt-in (manifest options.attribution, default local): when enabled it reads the provider key from the machine's magpie config and queries the configured remote magpie's read-only usage/requests endpoint (rows filtered to the configured callers). The key goes only to that remote magpie.
  • Brand logos are read from magpie's own plugin-providers.json (data URIs only, injected into the page): no sibling-package scanning, no third-party download, no disk writes. Brands without one fall back to initial-letter badges.
  • Anonymous routes are limited to /api/ping (service identity + build id) and /api/health (liveness) — no usage data, no tokens; listed in docs/community-submission.md.

Engineering facts

  • Zero runtime dependencies; the service is a single-file Node/Bun script with Host/token/origin guards and streaming log reads (capped memory, bounded samples). The page is loopback-only, tokens are per-boot and server-injected, APIs take the x-dash-key header only.
  • The UI ships in English and Chinese (131-key dictionary, ?lang= or navigator.language).
  • 32 tests cover auth, Host (DNS rebinding, :: wildcard, rewritten-Host page gate), path traversal, malformed requests, the lifecycle contract (import has no side effects, server() awaits listen), build-id-based port reuse, and the registry three-text rule.
  • Packaged: 272 KB tarball / 17 files; the vendored ECharts build (bar/line/pie/heatmap only) is reproducible from build/echarts-entry.mjs + scripts/build-echarts.mjs (npm run build:echarts, CI verifies byte-exact) with NOTICE + LICENSES/Apache-2.0.txt.
  • Docs shipped in the package: README, docs/data-semantics.md (the two-accounting-views red lines), docs/community-submission.md (this checklist, maintained as the source of truth for future updates).

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the board. It renders well on fixture data in Chromium and WebKit. Reviewed at 428613e. A few things block merging.

Blocking

  1. On magpie desktop the page returns 401. PLUGIN_MODE keys off MAGPIE_ADDR (server.mjs:21), which magpie's plugin host doesn't set, so openPage is false and / asks for a key the frame doesn't have (server.mjs:614-615). Please tell plugin mode apart some other way, and get the page its token through the bridge rather than relaxing auth.
  2. Remote attribution is on by default (ATTRIBUTION='auto', server.mjs:322). It sends the remote-magpie provider's API key to /v1/magpie/usage/requests for up to 40000 rows (server.mjs:361-375). The README calls it optional. Please make it opt-in.
  3. CI goes red. test/server.test.mjs:289 spawns index.mjs instead of running -e code, so the child never exits, keeps 3455 and hangs node --test. test/server.test.mjs:252 imports ./index.mjs with no cwd, so it fails under the root bun test. Root HOME=$(mktemp -d) bun test gives 3 failures in this package. Please also use port 0 and mkdtemp instead of fixed ports and fixed tmp names.
  4. When 3455 is taken, plugin mode binds 3456 while page/index.html and boot.js still point to 3455. The guard at server.mjs:890 never runs because :886 already calls tryBind(p+1). Separately, any process whose ping answers {service:'magpie-usage'} is reused as the board.
  5. The logo scan reads every sibling package's files and downloads the https icon URLs they declare (server.mjs:728-810). Please use the icons magpie already provides instead.
  6. The vendored static/echarts.min.js (sha256 791c7ea0…) doesn't match echarts@5.5.1/dist/echarts.min.js (e84270bd…), and the custom build entry isn't in the PR. Please use the upstream file, or include a reproducible build.

Please remove or change

  • Personal defaults: REMOTE_PROVIDERS, REMOTE_CALLERS, the merge map in static/index.html, and the Cloudflare analytics allowance in the CSP.
  • deploy.sh, the Dockerfile, docker-compose.yml and docs/self-hosting.md. They describe one VPS, vps-init.sh is missing, Dockerfile:13's COPY fails, and compose runs with no auth.
  • Origin: null and arbitrary localhost origins are accepted with CORS (server.mjs:476-496), and the key is accepted in ?key=. The page should be the only client.
  • Please add English strings (the UI is Chinese-only), a "option":"port" on the service, and drop the fake bridge hello (server.mjs:644).

What I ran (on a macOS test machine, HOME in a temp dir)

  • node --test in the package: 27 of 28 pass. "plugin mode waits…" fails and leaves a server on 3455.
  • bun scripts/check.mjs page-usage-board and the full scripts/check.mjs: pass. scripts/prepack.mjs: pass.
  • Root HOME=$(mktemp -d) bun test: 6882 pass, 3 fail, all in page-usage-board.
  • The service on fixture data with a fake gateway and remote, every fetch logged:
    • Without MAGPIE_ADDR, / returns 401, and the remote key is sent at startup.
    • With a foreign server on 3455 it binds 3456.
    • The logo scan fetched about 25 third-party URLs.
    • The Host checks rejected rebinding.
  • Playwright, Chromium and WebKit, at 1280 and 420 and in a sandboxed iframe: renders with no console errors, except that WebKit's framed view blocks /api/attribution under connect-src 'self'.

…n, magpie-sourced logos, reproducible echarts, en i18n

- / serves the board on loopback with the token injected (page-usage-tree model):
  no MAGPIE_ADDR/PLUGIN_MODE env detection, desktop 401 fixed; the page answers
  loopback Hosts only, and remote binds get 403 on the page + x-dash-key-only APIs
- ?key= and the dk cookie removed; tokens are per-boot, server-injected, never
  in URLs, logs or on disk
- port: upward scan removed; ~8s bind-retry then /api/ping BUILD_ID check —
  same build reuses (after a second liveness confirm), anything else errors
  loudly; manifest page.service.option="port", boot.js reads magpie.options
- remote attribution is opt-in (manifest options.attribution / ATTRIBUTION env,
  default local); REMOTE_CALLERS/REMOTE_PROVIDERS personal defaults removed
- logos: read magpie's own plugin-providers.json (data URIs only) and inject
  window.QLOGOS; the sibling-package scan and the ~25 third-party downloads are
  gone — zero network, zero disk writes
- CSP without third-party hosts, connect-src lists the loopback explicitly
  (WebKit sandboxed-frame fix); the fake bridge hello postMessage is dropped
- echarts custom build is reproducible now: build/echarts-entry.mjs +
  scripts/build-echarts.mjs rebuild static/echarts.min.js byte-exact
- full en/zh i18n (131 keys, ?lang= or navigator.language)
- deploy.sh / vps-init.sh / Dockerfile / docker-compose.yml / docs/docker.md /
  docs/self-hosting.md removed; tests use mkdtemp + ephemeral ports + -e
  children and are cwd-independent
@orinsid

orinsid commented Nov 8, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed review — all six blockers and the change list are addressed in 3dbe84b.

Blocking

  1. Desktop 401 — fixed by removing the env-based plugin-mode detection entirely. / now serves the board on loopback binds with the token injected into the HTML (the page-usage-tree model): no MAGPIE_ADDR/PLUGIN_MODE, no key the frame would need. The page additionally answers loopback Host names only (a proxied public name stays 403), non-loopback binds get 403 on the page and header-only APIs, and tokens are per-boot: injected server-side, never in a URL, log or on disk.
  2. Remote attribution — opt-in now. Default is local; sending the provider key to the remote magpie only happens after the user sets ATTRIBUTION=remote or the manifest option attribution: "remote" (the manifest ships options.attribution: "local"). The README and the submission doc state the key-leaves-this-machine consequence.
  3. CI / test robustness — the zombie-race test no longer spawns index.mjs as a file; every spawn child is node -e with an absolute-URL import, exits on all paths, and is SIGKILL-timed out by the parent. Tests use mkdtemp everywhere and ephemeral ports (the parent allocates a free port and passes it; the lifecycle child picks its own port). All paths resolve from the test file, so the root bun test works; the spawn target probes the runtime (falls back to node under bun).
  4. Port handling — the upward scan is gone. On EADDRINUSE the new instance retries binding for ~8s (a dying previous generation releases within that window), then asks /api/ping for identity: the same BUILD_ID (a sha256 over the seven runtime files) reuses after a second liveness confirmation 1.5s apart, and anything else fails loudly ("held by another build of this board"). The manifest declares "service": {"port": 3455, "option": "port"}, and boot.js reads the port from magpie.options.get(), so page and service always agree.
  5. Logo scan — removed. The service now reads magpie's own plugin-providers.json (data URIs only, ≤2MB each / ≤4MB total) and injects window.QLOGOS; there is no sibling-package scanning, no third-party download, no disk writes, and no /logos route. Brands without a data-URI icon fall back to the initial-letter badge. The anonymous surface is down to /api/ping (identity + build) and /api/health.
  6. echarts — the custom build is now reproducible from the repo: build/echarts-entry.mjs + scripts/build-echarts.mjs (npm run build:echarts) rebuild static/echarts.min.js byte-exact (sha256 791c7ea0…, verified locally and re-verified by CI with git diff --exit-code after an npm ci rebuild). The banner is part of the build script, so the provenance note is reproduced too.

Please remove or change

  • Personal defaults removed: REMOTE_PROVIDERS ships only magpie's own three line names (overridable via options.remoteProviders), REMOTE_CALLERS has no default, the personal merge map in the page is gone, and the Cloudflare allowance is out of the CSP (no third-party host remains anywhere in the CSP).
  • deploy.sh, vps-init.sh, Dockerfile, docker-compose.yml, .dockerignore, docs/docker.md and docs/self-hosting.md are deleted; the README now documents only the in-magpie mode and a plain node server.mjs standalone.
  • CORS now allows only Origin: null (sandboxed frame), no-Origin navigations, and the exact bound port on loopback — not arbitrary localhost ports. ?key= is gone; the page is the only client.
  • English strings added: the page ships a 131-key zh/en dictionary selected by ?lang= (the lead page passes the bridge language) or navigator.language. "option": "port" is on the service, and the fake bridge hello postMessage is dropped.
  • WebKit framed view: connect-src now lists http://127.0.0.1:<port> / http://localhost:<port> explicitly, so /api/attribution works in the sandboxed frame under WebKit.

Verified locally on Node 22/24: npm test 32/32 (including new regressions: same-build reuse vs. foreign-build rejection, ?key= dead, logo privacy, exact-port CORS, loopback-only page, :: wildcard, proxied-Host page gate), scripts/prepack.mjs and npm pack --dry-run pass, and the board renders in Chromium with zero console errors in both languages.

@yetone

yetone commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for the quick turnaround. I re-reviewed at 3dbe84b. Four of the six blockers are fixed, but CI would still go red and a few new problems came in with the rework, so this isn't ready to merge yet.

The six blockers

  1. Desktop 401: fixed. On a loopback bind the page is served without a key and the token is injected, with Host and Origin checks (server.mjs:590-630). This is the same model as page-usage-tree, so it's fine.
  2. Attribution opt-in: fixed. The manifest says local, and the env default is local (server.mjs:746). But no test guards it. baseEnv sets ATTRIBUTION: 'local' itself (test/server.test.mjs:30), so "attribution stays local unless the user opts in" still passes when I change the default to 'remote' by hand. Please leave ATTRIBUTION out of the env in that test.
  3. CI: still red.
    • bun test packages/page-usage-board has 31 pass and 1 fail. "reuses a live instance of the same build, never a different one" (test/server.test.mjs:333) waits through the 8s foreign-build deadline, which is longer than bun's 5s default timeout. Please give it a { timeout }, or make the deadline shorter in the test.
    • The root HOME=$(mktemp -d) bun test has 6859 pass and 1 fail. Its before hook (test/server.test.mjs:98) dies with Executable not found in $PATH: "node", so the whole server.test.mjs file is lost. Under Bun the file spawns node by name (test/server.test.mjs:16), and every test file shares one process, so the result depends on what earlier files did to process.env.PATH. For example, packages/gcp-ai/test/plugin.test.mjs:786 sets it to an empty directory. Please don't depend on PATH at spawn time.
  4. Port: fixed. There is no upward scan, a foreign or different-build occupier fails startup loudly, and the page reads the port from options. Breaking either check by hand turns a test red.
  5. Logos: fixed. Only data:image/ icons from plugin-providers.json are used, with size caps. Keeping https icons turns the logos test red. The file is found next to usage.jsonl, though, so it has the path problem below.
  6. echarts: rebuilt byte for byte with echarts 5.5.1 and esbuild 0.28.2 (sha256 791c7ea0…), which is good. But scripts/build-echarts.mjs:5-7 says a CI job compares bytes and that esbuild is pinned "per package-lock.json". The PR adds neither. Installing from package.json's ranges (^5.5.1) picks echarts 5.6.0, and that build differs. Please pin exact versions in devDependencies, or add the lockfile and the CI check, or drop the claim.

New problems

  • The gateway is hard-coded. GATEWAY defaults to http://127.0.0.1:3425 (server.mjs:25), and MAGPIE_ADDR is never read. magpie's plugin host always sets MAGPIE_ADDR to the real gateway address, which differs when the port is changed in Settings or on a portable install. I tested this live with MAGPIE_ADDR pointing at a fake gateway: the board logged gateway=http://127.0.0.1:3425, the fake gateway saw no requests, and the overview cards were empty. Please read it the way page-usage-tree does: options, then env, then MAGPIE_ADDR, then 3425.
  • The server reads magpie's files directly: usage.jsonl (server.mjs:27-31), plugin-providers.json, and providers.json with its keys in remote mode (server.mjs:52). The paths assume ~/.config/magpie, so they miss XDG_CONFIG_HOME and portable installs. If those reads stay, please follow magpie's config dir. Better still, read through the gateway's usage APIs, as page-usage-tree does.
  • The fake bridge hello is still there. Only the server-injected copy was removed; static/index.html:160-164 still posts {magpie:1,id:1,method:"hello"} to * at 0s, 3s and 8s.
  • The client still reads ?key= (static/index.html:1354). The server ignores it now, so please drop it.
  • The gateway error note is Chinese-only ('用量接口 ' + code, server.mjs:539 and :623). With ?lang=en the page shows "用量接口 0 — figures below may be incomplete".
  • Smaller points:
    • The theme follows prefers-color-scheme only, not magpie's own light/dark choice. magpie.theme from the bridge could be passed on in boot.js the way lang is.
    • startServer({port: 0}) binds 3455, not a random port.
    • The page CSP has no frame-ancestors.

What I ran (on a macOS test machine, Node 22.22, Bun 1.4.2, HOME in a temp dir)

  • In the package, node --test: 32 of 32 pass. bun test packages/page-usage-board: 31 pass, 1 fail (above).
  • bun scripts/check.mjs page-usage-board: pass. Prepack: pass. npm pack --dry-run: 17 files, 272.5 kB.
  • Root HOME=$(mktemp -d) bun test: 6859 pass, 20 skip, 1 fail (the server.test.mjs before hook). No board processes were left afterwards.
  • I broke each fix by hand in a copy and ran its tests. These went red as they should:
    • port scan;
    • reusing any build;
    • the page needing a key;
    • https logos;
    • CORS for any localhost port;
    • ?key= accepted.
      Defaulting attribution to remote stayed green (point 2).
  • echarts: rebuilt with the pinned versions (identical) and with the package.json ranges (differs).
  • Live: the server ran on fixture data with MAGPIE_ADDR set to a fake gateway. Playwright loaded it in a sandboxed iframe in Chromium and WebKit, light and dark, at 1280 and 420 wide. Every case rendered with no console errors, no horizontal scroll and only loopback requests. The gateway problem above showed up in every case.

…via bridge, test hardening, pinned devDeps + lockfile
@orinsid

orinsid commented Nov 8, 2026

Copy link
Copy Markdown
Author

Thanks for the re-review — all points addressed (pushed as 6450cbc on the PR branch).

Leftover blockers

  • Attribution guard — ATTRIBUTION is now stripped from that test's env (explicit undefined, so the child process never sees it and the server exercises its own default). The test also passes REMOTE_BASE/REMOTE_KEY: if the default were flipped to remote, the source would change and the test would go red instead of sitting green behind baseEnv's value.
  • bun timeouts / PATH —
    • The deadline-bound tests (reuses a live instance…, plus the new gateway/port-0 tests) run with { timeout: 30000 }, which both node:test and bun accept; the 8s foreign-build wait no longer hits bun's 5s default.
    • PATH is snapshotted once at module load (before any test in the shared bun process can clobber it) and passed explicitly in every child's env, so spawn("node") no longer dies with Executable not found after gcp-ai's test empties process.env.PATH.

New problems

  • Gateway — resolved per request-startup in the chain options.gateway → GATEWAY → MAGPIE_GATEWAY → MAGPIE_ADDR (a 0.0.0.0:/[::]: prefix is normalised to loopback) → the 3425 default, the same order page-usage-tree uses. There's a regression test that starts the board with MAGPIE_ADDR pointing at a fake gateway and requires the overview request to land on it (and one asserting an explicit GATEWAY wins over MAGPIE_ADDR).
  • Config paths — a magpieConfigDir() helper now feeds all three direct reads: MAGPIE_CONFIG_DIR → XDG_CONFIG_HOME/magpie (when absolute) → ~/.config/magpie. usage.jsonl's default candidates, plugin-providers.json and providers.json all resolve through it, and MAGPIE_CONFIG_DIR is documented in the README env table and .env.example.
  • hello — the remaining client-side postMessage block is deleted; the page no longer sends any bridge frames.
  • ?key= — the client's query-string read is gone too; the key only ever comes from the server-injected window.__KEY__ and travels as the x-dash-key header.
  • Server notes — the gateway/log error notes the page displays verbatim are English now (usage API 502, usage log read failed (…)); the banner structure and tail remain translated by the page dictionary.
  • Theme — boot.js reads current.dark from the bridge and passes ?theme=dark|light; the board pins data-theme from that parameter (or prefers-color-scheme when absent) in a <head> inline script before the stylesheet applies, so magpie's own choice wins over the OS and there's no flash. The light palette moved from the @media block to :root[data-theme="light"]; chart colours keep reading the vars at render time.
  • port: 0 — startServer({ port: 0 }) now binds an ephemeral port (tested), and index.mjs maps options.port of 0/invalid to the default so the page's port and the service's can never disagree.
  • frame-ancestors — added explicitly. It has to stay permissive: the board is designed to be framed by magpie's own window, and magpie's frame origin isn't a fixed value across desktop/webview/web, so any concrete allowlist would break one of those. The CSP states frame-ancestors * with a comment explaining why it cannot be tightened.

echarts claim vs. PR contents

devDependencies are pinned exact (echarts 5.5.1, esbuild 0.28.2), package-lock.json is now part of the package directory in the PR, and the CI byte-compare step stands (it runs npm ci before rebuilding, so the lockfile drives the versions). Verified the rebuild stays byte-identical under the pins.

Verification: npm test 34/34 (new: gateway chain, ephemeral port, attribution default guard), prepack and npm pack --dry-run pass (17 files), and the board was smoke-tested in Chromium for ?lang=en&theme=light (light palette applied before first paint, charts on light vars, zero console errors) plus the dark/zh cases from the previous round.

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I re-reviewed at 6450cbc. Most of the last round is fixed, but the root test run is still red, and the config dir still misses portable installs.

Blocking

  1. The root bun test is still red, with the same failure as last time. The PATH snapshot at test/server.test.mjs:18 doesn't help, because Bun loads each test file only when it reaches it. By then an earlier file has already changed process.env.PATH, so the snapshot is the broken value. Root HOME=$(mktemp -d) bun test on macOS (Bun 1.4.2): 6859 pass, 20 skip, 1 fail. The before hook (test/server.test.mjs:103 → start at :44) dies with Executable not found in $PATH: "node", and the whole server.test.mjs file is lost. It was the only failure in the repo. Please don't spawn node by name under Bun. For example, spawn process.execPath: that runs server.mjs under Bun, which is also what magpie's plugin host runs it with.
  2. The config dir still misses portable installs. magpieConfigDir() (server.mjs:27-32) tries MAGPIE_CONFIG_DIR, then XDG_CONFIG_HOME, then ~/.config/magpie. magpie has no MAGPIE_CONFIG_DIR, and a desktop user can't set env for the plugin host, so README.md:96's "portable/XDG installs set this" leaves portable users with an empty board. magpie's own order (appdir.Config()) is the portable data dir first, then XDG, then ~/.config/magpie. The plugin host starts with its cwd set to that dir (bunCommand(..., settings.Dir(), "run", js) in internal/plugin/host.go). So server() running in the host can take process.cwd(), or read through the gateway's usage APIs as page-usage-tree does. The ROOT/.. fallbacks (server.mjs:59-60) don't point at magpie's dir for an installed plugin either. README.md:96 also says XDG is "honoured first", but the code checks MAGPIE_CONFIG_DIR first.

Smaller

  • scripts/build-echarts.mjs:5-7 still says CI rebuilds the bundle and compares bytes. No workflow does that. Please drop the claim. (I rebuilt it myself with npm ci and got the same bytes, sha256 791c7ea0….)
  • static/index.html:308 and :447 (needKey) still tell the user to append ?key=…, which no longer works.
  • server.mjs:546 and :595 still return Chinese-only errors.
  • MAGPIE_ADDR of [::1]:<port> fails the check at server.mjs:48 and falls back to 3425.

Verified fixed

  • Attribution default: changing it to remote by hand turns "attribution stays local unless the user opts in" red.
  • Gateway: dropping the MAGPIE_ADDR read by hand turns the gateway test red.
  • echarts is pinned exactly (5.5.1, esbuild 0.28.2).
  • The hello frames are gone, and so is the client's ?key= read.
  • The theme comes from the bridge.
  • port: 0 gives an ephemeral port.
  • frame-ancestors is set.
  • With Origin: null, the page's HTML is served without Access-Control-Allow-Origin, so a null-origin fetch can't read the injected key. /api/* still needs x-dash-key.
  • The pages escape usage and quota strings before they go into innerHTML.

What I ran (macOS test machine, Node 22.22, Bun 1.4.2, temp HOME)

  • bun test packages/page-usage-board: 34 pass. node --test in the package: 34 pass. bun scripts/check.mjs: pass.
  • Root HOME=$(mktemp -d) bun test: 1 fail (above).
  • Hand breaks of the attribution default and the gateway read: red as expected.
  • echarts: npm ci plus npm run build:echarts in a copy of the package gave identical bytes.
  • No CI checks ran on this head.

@orinsid

orinsid commented Nov 8, 2026

Copy link
Copy Markdown
Author

Thanks — both blockers and the smaller points are addressed (pushed as 6b2625d on the PR branch).

Blocking

  1. No node by name, ever — all test children now run process.execPath: under bun test that is bun, which is exactly how magpie's host runs server.mjs, and under node --test it is node. The inline -e children became temp-file children (the code is written to a .mjs in a mkdtemp dir and run as a file), so there is no -e compatibility question and no PATH lookup anywhere; the PATH snapshot hack is deleted. The three children that only start the service (waits for a dying occupier, reuses a live instance…, startServer({port: 0})) and the lifecycle import-check all go through one spawnChild(code, env) helper.
  2. Config dir — rewritten to follow magpie's own appdir.Config() order, using the fact you pointed out about the host's cwd: the service takes process.cwd() as magpie's data dir when it carries the markers (usage.jsonl / plugins.json / providers.json), then XDG_CONFIG_HOME/magpie, then ~/.config/magpie. Since the plugin host starts the service with settings.Dir() as cwd, portable installs are covered with no env at all. MAGPIE_CONFIG_DIR stays only as an explicit override, documented as such (magpie has no such env — the README no longer implies users can set env for the host, and the "XDG honoured first" wording is fixed). The dead ROOT/.. fallback chain is removed. There's a regression test that spawns the server with cwd set to a data dir and no USAGE_JSONL env, and asserts the log resolves jsonl=<cwd>/usage.jsonl(ok).

Smaller

  • MAGPIE_ADDR normalises 0.0.0.0:, [::]: and [::1]: prefixes to loopback before validating; a bare [::1] is handled too.
  • The needKey texts in the page (zh + en) no longer mention ?key= — a 401 now says to reload the page so the freshly injected key is picked up.
  • server.mjs 401 (unauthorized: send the x-dash-key header) and 503 (usage.jsonl not readable) bodies are English.
  • scripts/build-echarts.mjs no longer claims a CI job; the header documents the verify command (node scripts/build-echarts.mjs && git diff --exit-code static/echarts.min.js) and the pins. README's command list says the same; NOTICE only ever said "reproducible with npm run build:echarts".

Verification: npm test 35/35 (new: host-cwd config-dir regression), prepack and npm pack --dry-run pass.

@yetone

yetone commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Thanks. I re-reviewed at 6b2625d. Both earlier blockers are fixed in the server. But the new config-dir test is red on macOS, so the root bun test is still red. The [::1] change also points the board at the wrong address.

Blocking

  1. "config dir: the host cwd carrying magpie markers wins (portable installs)" (test/server.test.mjs:307) fails on macOS under both node --test and bun test. The server is right; the test compares the wrong path. On macOS os.tmpdir() is /var/folders/…, which is a symlink to /private/var/folders/…. A child started with cwd: dataDir gets the resolved path from process.cwd(), so the log says jsonl=/private/var/folders/…/usage-board-cwd-lML9qb/usage.jsonl(ok), and the includes('jsonl=' + path.join(dataDir, …)) check at :336 fails. Please compare with realpathSync(dataDir), or make dataDir real before you spawn. It probably passes on Linux, where /tmp isn't a symlink, which may be why you didn't see it. I didn't run it on Linux.

    • Package node --test: 34 pass, 1 fail. bun test packages/page-usage-board: 34 pass, 1 fail.
    • Root HOME=$(mktemp -d) bun test: 6891 pass, 1 fail. This is the only failure in the repo.
  2. MAGPIE_ADDR=[::1]:<port> now goes to the wrong address (server.mjs:57-59). [::1]: and ::1: are rewritten to 127.0.0.1:, but a gateway listening on [::1] usually isn't listening on 127.0.0.1. Only the unspecified addresses (0.0.0.0, [::]) should be mapped to loopback. That is all magpie's own hostAddr in internal/plugin/host.go does. Your new regex already accepts [::1]:port as written, so dropping \[::1\]|::1 from the replace, and the second .replace, fixes it. A test with a fake gateway on ::1 would guard it.

    • To reproduce: put a fake gateway on ::1 only, then start the board the way the host does, with MAGPIE_ADDR=[::1]:65032. The log shows gateway=http://127.0.0.1:65032, the fake gateway gets no requests, /api/overview says usage API no response, and the page shows no quota cards. With the gateway on 127.0.0.1 and MAGPIE_ADDR=127.0.0.1:<port>, every request arrives and the cards render.

Verified fixed

  • Tests no longer spawn node by name. In the root run server.test.mjs runs in full, and its only failure is the macOS path above. At 6450cbc the whole file was lost in its before hook.
  • Portable installs: I started the board the way the plugin host does. Bun imported index.mjs and called server({}, {port}) with cwd set to a separate data dir, and HOME had no ~/.config/magpie. The board read that dir's usage.jsonl and plugin-providers.json: 30 days, 17 hours, attribution totals and the logo all came from it. With the cwd lookup removed by hand, the config-dir test goes red, so it would guard this once its path compare is fixed. Removing the MAGPIE_ADDR read by hand turns the gateway test red.
  • needKey no longer mentions ?key=. The 401 and 503 bodies are in English. build-echarts.mjs no longer claims a CI job.
  • The rest of the diff is unchanged from 6450cbc, which I reviewed last round. It merges cleanly onto current main.

On overlap with magpie's Usage page
Much of this board is also on magpie's own Usage page: the allowance cards, the per-provider and per-model totals, and the request list. What only this board has is the remote attribution by caller key, TTFT p50/p90 per provider, the 7×24 heatmap, the direct-vs-remote split of the last 24h, and expiry tiers on the cards. That is enough for an opt-in page, so I'm not blocking on it. Please keep the README honest that most of it repeats Usage, and lead with what is new.

What I ran (macOS test machine, Node 22.22, Bun 1.4.2, temp HOME outside /tmp)

  • Package node --test: 34 pass, 1 fail. bun test packages/page-usage-board: 34 pass, 1 fail. Both failures are the config-dir test.
  • bun scripts/check.mjs page-usage-board: pass. Prepack: pass. npm pack --dry-run: 17 files, 274.3 kB.
  • Root HOME=$(mktemp -d) bun test: 6891 pass, 1 fail (same test). No board processes were left afterwards.
  • Hand breaks: no cwd lookup made the config-dir test red under node and bun. No MAGPIE_ADDR read made the gateway test red.
  • Live: the host-style start above, in a sandboxed iframe, in Chromium and WebKit, light and dark, at 1280 and 420 wide. No console errors, no horizontal scroll, only loopback requests, and the service process was gone afterwards. I ran it with MAGPIE_ADDR=[::1]:… (blocker 2) and with 127.0.0.1:… (everything works).

@orinsid

orinsid commented Nov 8, 2026

Copy link
Copy Markdown
Author

Both blockers fixed (pushed as ebb1f29), and the README now leads with what is new.

1. macOS config-dir test — the test now resolves the temp dir first: it compares against realpathSync(dataDir) and also spawns the child with that real path as cwd. On macOS os.tmpdir() is /var/folders/… and /var is a symlink to /private/var, so the child's process.cwd() — and therefore the jsonl= line — carries the resolved path. You were right that Linux hides this: /tmp isn't a symlink here, which is why it passed locally. Comment in the test records the reason so it doesn't regress.

2. MAGPIE_ADDR=[::1]:<port> — only the unspecified addresses are mapped now: 0.0.0.0 and [::] become 127.0.0.1, everything else is kept as written, matching magpie's own hostAddr. [::1]:port therefore resolves to http://[::1]:port and reaches a gateway that only listens there. While fixing it I also found a real bug in the rewrite I'd added: the replacement used $1 (the address group) instead of $2 (the separator), so 0.0.0.0:10099 came out as 127.0.0.10.0.0.010099, failed the host:port check, and silently fell back to 3425. Both forms are now covered:

  • the existing test asserts MAGPIE_ADDR=0.0.0.0:<port> logs gateway=http://127.0.0.1:<port> and the request lands there;
  • a new test puts a fake gateway on ::1 only, starts the board with MAGPIE_ADDR=[::1]:<port>, and requires gateway=http://[::1]:<port> (and not 127.0.0.1:<port>) plus a served overview from that gateway. The end-to-end half is skipped only when the platform itself cannot reach the IPv6 loopback (this Windows box can't: a server listening on ::1 accepts the listen but fetch times out), so the resolution assertion still runs everywhere.

npm test 36/36, prepack and npm pack --dry-run pass (17 files).

README — it now opens by saying most of the board repeats magpie's Usage page (cards, per-provider/per-model totals, request list) and lists the five things Usage doesn't show: remote attribution by caller key, TTFT p50/p90 per provider, the 7×24 heatmap, the direct-vs-remote split of the last 24 hours, and expiry tiers on the cards — then says everything else is convenience around those. I left the registry summary untouched (the three-text rule pins it to the registry entry and the PR description); say the word if you'd rather that text changed too and I'll update all three together.

Also removed a stale README sentence claiming the board posts a bridge hello — that went away two rounds back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants