Troubleshooting
Issues we've actually hit, with root causes and fixes. Symptom → cause → fix.
If you hit something not here, open an issue.
Install / scaffold
Command "i99dash" not found
Symptom: pnpm exec i99dash <command> errors with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL.
Root cause: i99dash is installed as a devDependency, so the binary lives in node_modules/.bin/. If node_modules/ is missing (you deleted it, moved the project without it, or skipped pnpm install), the binary isn't there.
Fix:
pnpm install
pnpm exec i99dash --versionIf you moved the project to a fresh directory, always re-run pnpm install — pnpm symlinks per-project.
Backend URL override
If the default https://api.i99dash.app doesn't resolve from your network (firewalled lab, self-hosted backend), point the CLI at the right host either per-command or via env:
i99dash login --backend-url <URL>
# or, for the rest of the shell session
I99DASH_BACKEND_URL=<URL> i99dash loginLogin
i99dash login exits with SSH_AUTH_DISABLED
Symptom: login fails immediately with "SSH-key auth is not enabled on this deployment".
Root cause: the backend's auth.ssh_enabled runtime flag is off. SSH login is gated until an administrator turns it on.
Fix: ask your i99dash administrator to flip auth.ssh_enabled on (Admin → Settings → Runtime config — it takes effect fleet-wide, no redeploy).
Login fails right after "signing in with key…" (SSH_SIGNATURE_INVALID)
Symptom: the CLI prints signing in with key SHA256:… then errors with "this key isn't registered yet" / SSH_SIGNATURE_INVALID.
Root cause: the public half of the key you're signing with isn't on your account. The server hands out a challenge nonce for any fingerprint (so it never reveals which keys exist), so an unregistered — or simply wrong — key only fails at the final verify step.
Fix: copy ~/.ssh/id_ed25519.pub into the web console (Account → SSH keys), then re-run i99dash login. If you pass --key, make sure its public half is the one you registered.
Publish
403 Forbidden on presigned PUT during i99dash publish
Symptom: publish succeeds at validation + build, then fails on the bundle upload step.
Root cause (one of):
- The bucket's CORS rule doesn't include the origin you're publishing from. The dev-server runs on
http://localhost:5173; if your local has shifted to a different port, CORS rejects the PUT. - The presigned URL expired (default 5 min from issue). Slow networks on large bundles can hit this.
Fix:
- Restart
i99dash devon the default port (5173). - Re-run
publish(it requests a fresh presigned URL).
publish succeeds but the catalog never shows the app
Symptom: terminal reports successful upload + register, but the admin dashboard's mini-apps list doesn't show your app.
Root cause (likely): the admin dashboard catalog page caches the manifest for a few seconds. Or your account doesn't have catalog read permission.
Fix:
- Hard-refresh the admin page (Cmd/Ctrl + Shift + R).
- Confirm
whoamishows your account is a developer (i99dash whoami).
Backend
enable_sdk is true but the mini-apps endpoints still 404
Symptom: you flipped ENABLE_SDK=true in DO App Platform, redeployed, but /api/v1/mini-apps/* still 404s.
Root cause: the enable_sdk setting is gated by a model_validator that checks all four MINIAPPS_SPACES_* env vars are non-empty before honoring the flag. If any one is missing or set to placeholder, the validator forces enable_sdk=false regardless.
Fix: confirm all four are set with real values (mint a Spaces key, paste both halves):
MINIAPPS_SPACES_ENDPOINT(e.g.https://fra1.digitaloceanspaces.com)MINIAPPS_SPACES_BUCKET(e.g.i99dash-miniapps)MINIAPPS_SPACES_ACCESS_KEY(SECRET, real value)MINIAPPS_SPACES_SECRET_KEY(SECRET, real value)
The backend's prestart.sh logs enable_sdk forced to false: missing X, Y, Z when this happens — check doctl apps logs <app-id> --type=deploy for that line.
Host bridge
NotInsideHostError outside the host (Storybook, SSR, Node)
Symptom: AdminClient.fromWindow() throws NotInsideHostError: window is undefined or host bridge missing on window.
Root cause: there is no host bridge — your code is running outside the i99dash host (server-rendered, in a unit-test runner, or in Storybook).
Fix: guard the construction and render a fallback:
import { AdminClient, NotInsideHostError } from "i99dash";
let client: AdminClient | null = null;
try {
client = AdminClient.fromWindow({ context, catalog });
} catch (err) {
if (err instanceof NotInsideHostError) client = null;
else throw err;
}For tests, inject a fake bridge directly via the SDK's test helpers — see the runtime client docs.
CI / GitHub Actions
422 Unprocessable Entity on npm publish with --provenance
Symptom:
npm error 422 - Error verifying sigstore provenance bundle:
Unsupported GitHub Actions source repository visibility: "private".
Only public source repositories are supported when publishing with provenance.Root cause: npm's sigstore provenance attestation requires the source GitHub repo to be public. Publishing from a private repo with NPM_CONFIG_PROVENANCE: 'true' always fails.
Fix (pick one):
- Make the repo public (recommended for SDK distribution — packages are public anyway).
- Drop
--provenancefrom the workflow. You lose the sigstore-signed link between commit and tarball, but classic-token publish keeps working.
gh workflow run doesn't actually run the publish job
Symptom: you trigger the workflow, it completes in seconds with green check, but no packages were published.
Root cause: the publish job is gated on releases_created == 'true'. When release-please runs and finds no qualifying conventional commits, it reports false, the publish job is skipped, and the workflow completes successfully without publishing.
Fix: trigger with force_publish: true (manual recovery path):
gh workflow run release-please.yml \
--repo i99dash/i99dash-sdk \
-f force_publish=trueOn-device testing
These issues appear when you publish a mini-app to the catalog and
load it on a real i99dash host (instead of i99dash dev). They
are catalog / build-pipeline gaps the local dev-server doesn't
surface.
INVALID_RESPONSE on every family read (stale .strict() schemas)
Symptom: every client.<family>.getSnapshot() call returns
InvalidResponseError: invalid host response: <family> payload did not
match schema (see docs/api-reference/errors.md#invalid_response)…on media.read, vehicle.environment, climate.read, etc. The transport succeeded — only the parser rejects.
Root cause: a stale published i99dash (≤ 0.1.7) bundled .strict() Zod schemas for every family snapshot. .strict() rejects extra fields, so any forward-compatible field the host adds breaks parsing.
Fix: bump to the latest i99dash package — the consolidated build dropped .strict() from family schemas.
# in your mini-app
pnpm add i99dash@latest
pnpm bundle && i99dash publishIf you need to test a local SDK build before it ships:
# in i99dash-sdk/
pnpm pack --pack-destination /tmp/sdk-pack
# in your mini-app
pnpm install /tmp/sdk-pack/i99dash-1.0.0.tgz
pnpm bundle && i99dash publishVerify with grep -c '.strict()' src/i99dash-bundle.js — only the matched comment line should remain (count ≤ 2).
Mini-app stuck on "Loading…" / page script never runs
Symptom: your mini-app's static placeholder text (Loading…, v0.0.0) stays on screen forever. No JS errors visible (the host's WebView console isn't piped anywhere you can read it from outside).
Root cause: an ESM import { X } from './bundle.js' in your index.html references a symbol that isn't exported by the bundle — esbuild's tree-shaker drops export * re-exports the entry doesn't directly use. The browser fails the module load with
SyntaxError: The requested module './bundle.js' does not provide
an export named 'NotInsideHostError'…and the script never runs, so the static HTML stays untouched.
Fix: in your bundle entry (i99dash-bundle.entry.js), explicitly re-export anything the page imports for instanceof checks:
export * from "i99dash";
export * from "i99dash";
// esbuild's tree-shaker drops re-exports the bundle root doesn't
// reference. Pin classes the page uses for `instanceof` checks
// (NotInsideHostError, AdminClient, etc.) so they survive.
import { NotInsideHostError as _NotInsideHostError } from "i99dash";
export { _NotInsideHostError as NotInsideHostError };To debug from outside the host: load the same bundle.js in Node and inspect Object.keys(module) to confirm what's actually exported.
Mini-app blank on L5 / L5 Ultra / Song Plus but works on L8
Symptom: your mini-app loads fine on Leopard 8 (Di5.1) but renders a blank screen — or only the static HTML scaffolding — on Leopard 5, Leopard 5 Ultra, Song Plus, or any other Di5.0 trim. No JS error fires; even an inline window.addEventListener('error', …) sentry stays silent.
Root cause: Di5.0 trims ship with an Android System WebView frozen at whatever Chromium build was current when BYD released the platform (~early 2022). That WebView predates <script type="module"> support (Chrome 61, August 2017). When the WebView encounters a type="module" script tag, it silently does not execute it — no error event fires, the script just never runs. The page stays at whatever the static HTML rendered.
Di5.1 trims (L8, L5 Lidar, BYD HAN L) ship a much newer WebView (Chrome 100+) that handles modules + most ES2022 syntax fine, which is why the same bundle works there.
Fix: bundle as a classic IIFE script, downcompile to ES2019, drop the type="module" attribute. One-line change in three places:
// package.json
"scripts": {
- "bundle": "esbuild src/i99dash-bundle.entry.js --bundle --format=esm --target=es2022 --outfile=src/i99dash-bundle.js"
+ "bundle": "esbuild src/main.js --bundle --format=iife --target=es2019 --outfile=src/app.bundle.js"
} // src/main.js — pull SDK directly, no separate bundle entry needed
- import { MiniAppClient, NotInsideHostError } from './i99dash-bundle.js';
+ import { MiniAppClient, NotInsideHostError } from 'i99dash'; <!-- src/index.html -->
- <script type="module" src="./main.js"></script>
+ <script src="./app.bundle.js"></script>After this you can also delete src/i99dash-bundle.entry.js and src/i99dash-bundle.js — esbuild follows imports from main.js directly and rolls in the SDK + any third-party deps.
Why these targets: --format=iife produces a single classic <script> (no module loader needed; runs on every Chromium ≥ 30, ~2014). --target=es2019 downcompiles modern syntax (private class fields, optional chaining, nullish coalescing, etc.) so the SDK 2.0 source — written for ES2022 — runs on the older WebView's parser. Bundle gets ~5-15% larger; that's the entire cost.
To verify which version actually loaded on the device (catalog cache can lag the publish by minutes), add a tiny chip to index.html:
<div
id="__boot_chip"
style="position:fixed;bottom:4px;right:4px;z-index:99998;
background:rgba(0,0,0,.6);color:#fff;font:11px monospace;
padding:2px 6px;border-radius:3px"
>
boot v0.6.0
</div>The chip renders from the static HTML alone — if you don't see it, the bundle never even reached the device. Strip the chip after the issue is understood.
Don't: target ES2018 or older to "be safe." ES2019 is enough; lowering further trades bundle size for compatibility you don't need (Chromium 30 already handles ES2018).
See also: Build for L5 + L8 for the full resilient-build path, and Targets — trims, WebView & host for which trims need this.
Catalog auto-pull on launch overwrites direct edits to the on-device bundle
Symptom: you copy a freshly-built i99dash-bundle.js into the on-device install directory, but the WebView opens the old bundle anyway. Or the edit takes effect once and reverts on the next launch.
Root cause: the host re-syncs from the catalog on every mini-app open. Any direct edit to the on-disk bundle drifts from the catalog hash and gets overwritten.
Fix: bump manifest.json's version and run i99dash publish. There's no shortcut for production-shape testing. For pure SDK-bundle iteration, run i99dash dev against the fixture bridge instead — that path serves your local bundle directly without catalog round-trip.
Mini-app version on screen doesn't match manifest.json
Symptom: you bumped manifest.json to 0.3.7 and published, but the mini-app header still reads v0.3.0.
Root cause: many starter templates hardcode the displayed version separately from the manifest:
<span class="subtitle" id="version">v0.1.0</span>const APP_VERSION = "0.3.0";
versionEl.textContent = `v${APP_VERSION}`;The static span shows when the script never ran (see "stuck on Loading"); the JS-set value shows when the script ran but the constant is stale.
Fix: keep both in sync with the manifest. Either bump them by hand on every release, or replace at bundle time:
sed -i '' "s/const APP_VERSION = '[^']*'/const APP_VERSION = '$(jq -r .version manifest.json)'/" src/index.html…and run that as a prebundle step in package.json.