Virtual Runtime Environment
A drop-in browser-based preview shell with a full devtools API. No servers. No build step. Drop in two scripts and get a live preview with console, network, DOM inspector, and a programmatic API for agents, IDEs, and sandbox tools.
Quick start
One file is required. Drop it into your project alongside your virtual filesystem definition.
| File | Purpose |
|---|---|
| https://preview.codevre.dev/v1/vre.min.js | Core shell — classic script tag |
| https://preview.codevre.dev/v1/vre.esm.js | Core shell — ES module import |
Minimal HTML — classic script:
<!-- 1. Load styles and scripts --> <script src="https://preview.codevre.dev/v1/vre.min.js"></script> <div id="vre"></div> <script src="file-system.js"></script> <script> // Basic mode const vre = VRE.mount({ ownerId: 'YOUR_OWNER_ID', container: "#vre", fileSystem, entryPath: "/index.html", devtoolsOpen: true, }); vre.run(); // OR strict backend-protected mode const secureVre = VRE.mount({ accessCheckUrl: 'https://your-backend.com/validate', container: "#vre", fileSystem, entryPath: "/index.html", devtoolsOpen: true, }); secureVre.run(); </script>
Minimal HTML — ES module:
<div id="vre"></div> <script src="file-system.js"></script> <script type="module"> import { VRE } from "https://preview.codevre.dev/v1/vre.esm.js"; const vre = VRE.mount({ ownerId: 'YOUR_OWNER_ID', container: "#vre", fileSystem: window.fileSystem, entryPath: "/index.html", devtoolsOpen: true, }); vre.run(); </script>
For stricter deployments, use accessCheckUrl instead of ownerId. This
allows your backend to securely exchange a private API key for a short-lived runtime token without
exposing the API key in the browser.
Your accessCheckUrl endpoint should validate the current user, session, or application
before exchanging a runtime token. Do not expose a public endpoint that blindly returns runtime
tokens to any caller.
The basic ownerId flow is still protected through authorized domain restrictions
configured in the dashboard.
File structure
your-project/ ├── index.html ← your demo page ├── https://preview.codevre.dev/v1/vre.min.js ← core shell (required) └── file-system.js ← your virtual filesystem
Wrap your filesystem in an IIFE and
attach to window to avoid variable collision with the shell:
(function () { const fileSystem = { // ... your filesystem object }; window.fileSystem = fileSystem; })();
Virtual filesystem
A plain JSON tree of directory and file nodes. Pass it to mount() as
fileSystem.
VRE’s virtual filesystem is intentionally lightweight and runtime-focused. It does not attempt to fully replicate every Node.js filesystem, process, or module abstraction directly. For advanced compatibility layers, you can pair the VFS with external shims such as BrowserFS, persistence adapters, or custom runtime bridges while keeping them synchronized with the VRE filesystem state.
const fileSystem = { id: "my-project", name: "my-project", path: "/", type: "directory", children: [ { id: "idx_html", name: "index.html", path: "/index.html", type: "file", extension: "html", mimetype: "text/html", content: `<!DOCTYPE html>...`, }, { id: "style_css", name: "style.css", path: "/style.css", type: "file", extension: "css", mimetype: "text/css", content: `body { margin: 0; }`, }, { id: "app_js", name: "app.js", path: "/app.js", type: "file", extension: "js", mimetype: "application/javascript", content: `console.log("hello");`, } ] };
Nested directories:
{
id: "src_dir",
name: "src",
path: "/src",
type: "directory",
children: [
{
id: "main_js",
name: "main.js",
path: "/src/main.js",
type: "file",
mimetype: "application/javascript",
content: `export const hello = "world";`
}
]
}
File source types:
| source | Description | Required fields |
|---|---|---|
| "vfs" | Content lives in the filesystem object (default) | content |
| "host" | Fetched from your server at run time | hostPath, content: null |
| "url" | Loaded natively by the browser from a CDN | url, content: null |
// VFS file (default — content is inline) { type: "file", source: "vfs", path: "/app.js", content: `...` } // Host file (fetched from your server before run) { type: "file", source: "host", path: "/logo.png", hostPath: "/assets/logo.png", content: null } // URL file (browser loads it natively — appears in Sources panel) { type: "file", source: "url", path: "/vendor/chart.js", url: "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js", content: null }
Mount options
| Option | Type | Default | Description |
|---|---|---|---|
| ownerId | string | null | Basic access mode. Your Codevre owner ID. Either ownerId or
accessCheckUrl is required.
|
| accessCheckUrl | string | null | Strict access mode. Backend endpoint that validates the current app, user, or session before exchanging a private API key for a short-lived runtime token. |
| container | string | Element | — | Where to mount the shell (required) |
| fileSystem | object | null | Virtual filesystem tree |
| entryPath | string | "/index.html" | File to load on run |
| src | string | null | Load a real URL instead of VFS. Bypasses runtime protection — don't use for secured deployments. |
| devtoolsOpen | boolean | false | Open devtools panel on start |
| devtoolsHeight | number | 240 | Devtools panel height in px |
| theme | string | object | 'dark' | Shell colour theme: 'dark', 'light', or a token object
|
| blockPatterns | string[] | [] | URL substrings to hide from console, network, and sources panels |
| cssPath | string | vre.css | URL to a custom stylesheet. Overrides the default shell styles entirely. |
Custom stylesheet:
The shell loads styles.min.css by default.
You can download an unminified version of that file as a starting point, modify it, host it, and pass
your version via
cssPath.
const vre = VRE.mount({ ownerId: 'YOUR_OWNER_ID', container: "#vre", fileSystem, entryPath: "/index.html", cssPath: "https://your-cdn.com/your-vre-theme.css", // ← your custom stylesheet });
Filesystem API
All filesystem methods are synchronous and return api for chaining
unless noted.
Read the content of a file from the VFS. Returns null if the
file does not exist.
const html = vre.readFile('/index.html'); const css = vre.readFile('/style.css'); console.log(html.length); // character count
Update an existing file's content and hot-reload the preview. Throws if
the file does not exist — use addFile to create new files.
vre.updateFile('/style.css', 'body { background: red; }');
Add a new file to the VFS. Throws if the path already exists unless
overwrite is true.
vre.addFile('/utils.js', 'export const add = (a, b) => a + b;'); vre.addFile('/style.css', 'body {}', { overwrite: true }); vre.addFile('/data.json', '{"key":"value"}', { mimetype: 'application/json' });
Remove a file from the VFS. Throws { code: 'NOT_FOUND' } if
the path does not exist.
vre.deleteFile('/old-script.js');
Move or rename a file within the VFS.
vre.renameFile('/app.js', '/src/app.js');
List all files in the VFS as a flat metadata array. Does not include directory entries.
const files = vre.listFiles(); const jsFiles = vre.listFiles({ extensions: ['js'] }); const srcFiles = vre.listFiles({ prefix: '/src' });
Line-by-line diff between the current file content and a new string.
const diff = vre.diffFile('/app.js', newCode); diff.forEach(line => { if (line.type === 'add') console.log('+', line.text); if (line.type === 'remove') console.log('-', line.text); });
Save and restore the entire filesystem state.
snapshotFilesystem() returns an opaque token string.
const token = vre.snapshotFilesystem(); vre.updateFile('/style.css', 'body { background: red; }'); vre.restoreSnapshot(token); // rolls back
Replace or read back the entire filesystem object.
vre.setFileSystem(newFs, { entryPath: '/index.html' }); const fs = vre.getFileSystem();
Lifecycle API
run() starts or restarts the preview. stop()
returns to idle. reload() is an alias for run().
destroy() stops and removes the shell from the DOM entirely.
vre.run(); vre.run(newFs, { entryPath: '/index.html' }); vre.stop(); vre.destroy();
getStatus() returns a plain serialisable object safe to pass
directly to an LLM. isRunning() and isReady() return booleans.
const status = vre.getStatus(); // { running, ready, entryPath, inspecting, activeTab, devtoolsOpen }
onReady() resolves when the engine becomes ready
(immediately if already ready). waitForLoad() resolves when the preview
finishes loading. reset() hard-resets and clears all devtools state.
await vre.onReady(); vre.run(); const { entryPath } = await vre.waitForLoad({ timeout: 15000 }); vre.reset({ fileSystem: newFs, entryPath: '/index.html' });
Navigation API
Navigate to a path. open() starts the preview if not already
running. Pass { push: false } to skip history.
vre.open('/about/index.html'); vre.open('/contact.html', { push: false }); vre.back(); vre.forward();
Set the entry path without navigating. getEntryPath()
returns the current entry path string.
vre.setEntryPath('/about/index.html'); console.log(vre.getEntryPath()); // '/index.html'
setViewport() accepts a named preset ('mobile'
390px, 'tablet' 768px, 'desktop' full) or an explicit
{ width } object.
vre.setViewport('mobile'); vre.setViewport({ width: 480 }); const history = vre.getHistory(); // ['/index.html', '/about/index.html']
Open or close the preview in a separate browser window.
vre.setExternalMode(true); // opens external window vre.setExternalMode(false); // returns to embedded preview
Devtools data API
Programmatic access to console, network, and sources panels — without reading the UI.
All captured console entries since last clear. Filter by
level, filter (text), or limit.
getErrors() is a shorthand for error-level only.
const errors = vre.getConsoleLogs({ level: 'error' }); const recent = vre.getConsoleLogs({ limit: 20 }); const errs = vre.getErrors();
All captured network requests since last clear. Filter by
method, status, filter, or limit.
const failed = vre.getNetworkLog({ status: 404 }); const api = vre.getNetworkLog({ filter: '/api/' });
All source files captured by the engine, as a flat array.
const sources = vre.getSources(); // [{ domain: 'my-project', path: '/app.js', fullUrl: '/app.js' }, ...]
Set the maximum number of console/network entries kept in memory. Default is 500.
vre.setLogBuffer(1000);
Tab options: 'console' · 'network' ·
'sources' · 'elements'. getActiveTab() returns the
currently active tab id. getDevtoolsOpen() returns true if the
panel is visible. clearDevtools() clears all
panels at once.
vre.switchTab('network'); console.log(vre.getActiveTab()); // 'network' vre.setDevtoolsOpen(true); if (!vre.getDevtoolsOpen()) vre.setDevtoolsOpen(true); vre.clearDevtools();
Manually refresh the Sources or Network panel.
vre.refreshSources(); vre.refreshNetwork();
DOM inspector API
getDOM() returns the last full DOM snapshot (async).
queryDOM() runs a CSS-selector query against it (sync). Supports: tag,
.class, #id, [attr], [attr=value].
await vre.getDOM({ fresh: true }); const buttons = vre.queryDOM('button'); const cards = vre.queryDOM('.card'); const inputs = vre.queryDOM('[type=text]'); console.log(buttons[0].tag); // 'button' console.log(buttons[0].attributes); // [{ name, value }, ...] console.log(buttons[0].path); // [0, 1, 3, ...] — path for getElement()
Get full inspect detail for a DOM node by its path array (from
queryDOM()). Returns tag, id, classList, computedStyle, boundingRect, and more.
const el = await vre.getElement(buttons[0].path); console.log(el.tag); // 'button' console.log(el.classList); // ['btn', 'btn-primary']
Shorthand — returns just the computed styles map for a node.
const css = await vre.getComputedStyles(buttons[0].path); console.log(css['font-size']); // '13px'
getBoxModel() returns the full CSS box model (margin,
border, padding, content). getLayout() returns just the bounding box in iframe
viewport coordinates.
const box = await vre.getBoxModel(buttons[0].path); // { margin, border, padding, content: { x, y, width, height } } const rect = await vre.getLayout(buttons[0].path); // { x: 32, y: 88, width: 94, height: 32 }
Returns a role/name accessibility tree derived from the DOM snapshot.
const a11y = await vre.getAccessibilityTree(); // { role: 'generic', tag: 'html', children: [...] }
Highlight a DOM node in the preview (same as hovering in the Elements
panel). clearHighlight() removes any active highlight overlay.
vre.highlightElement(buttons[0].path); // by path array vre.highlightElement('.hero'); // by CSS selector vre.clearHighlight();
patchStyle() live-patches a CSS property and writes back to
the VFS source. applyCSS() injects arbitrary CSS without touching the VFS —
pass { id } to overwrite on subsequent calls.
vre.patchStyle('.hero', 'background', 'red'); vre.patchStyle('.card', 'padding', '32px', { sourceFile: '/style.css' }); vre.applyCSS('.hero { display: none; }', { id: 'hide-hero' });
refreshDOM() forces a fresh DOM snapshot and re-renders the
Elements panel. inspectStart() activates the element picker.
inspectStop() deactivates it. isInspecting() returns
true if the picker is active.
vre.refreshDOM(); vre.inspectStart(); if (vre.isInspecting()) vre.inspectStop();
Agent / AI interface
Designed for AI agents, automated testing, and IDE integrations.
The single most important agent method. Returns a complete serialisable snapshot of everything — status, files, DOM, errors, logs, network — in one call. Pass this directly into an LLM prompt.
const ctx = await vre.getContext({ includeDom: true, // default true includeFiles: true, // default true domDepth: 4, // DOM tree depth logLimit: 50, // max log entries }); // ctx: { status, files, errors, consoleLogs, networkLog, sources, dom, timestamp }
Evaluate a JavaScript expression inside the live preview iframe.
evalAsync() wraps code in an async IIFE for await expressions.
Rejects with EVAL_ERROR or TIMEOUT.
const title = await vre.eval('document.title'); const data = await vre.evalAsync('return await fetch("/api").then(r => r.json())');
Take a PNG screenshot of the current preview viewport. Returns an
ArrayBuffer of PNG bytes. captureElement() is a shorthand for
capture({ selector }).
const png = await vre.capture(); const cardPng = await vre.captureElement('.hero');
Simulate a click by CSS selector or page coordinates.
await vre.click('#btn-run'); await vre.click(100, 200); // by coordinates
Type text into an input or textarea. Fires input and
change events. Pass { clear: true } to clear existing value first.
await vre.type('#email', 'user@example.com', { clear: true });
scroll() accepts { x, y } or
{ selector }. hover() fires mouseenter and
mouseover. focus() focuses an element.
await vre.scroll({ selector: '.footer' }); await vre.scroll({ x: 0, y: 500 }); await vre.hover('.card'); await vre.focus('#search-input');
Poll the live preview until a condition is met. All reject with
{ code: 'TIMEOUT' } on expiry. waitForConsole() accepts a string
or predicate function. waitForRequest() accepts a substring or regex.
await vre.waitForElement('.results', { timeout: 10000 }); await vre.waitFor('window.appReady === true'); await vre.waitForConsole('[app.js] Init complete'); const req = await vre.waitForRequest('/api/data', { method: 'POST' });
Run a declarative sequence of API actions. Never throws — collects errors
and returns { results, errors }.
const { results, errors } = await vre.runScript([ { action: 'eval', args: ['document.title'] }, { action: 'click', args: ['#btn-run'] }, { action: 'waitForConsole', args: ['Init complete'] }, { action: 'capture', args: [] }, ]);
Event system
Subscribe to engine events to react to the preview in real time.
Standard subscribe/unsubscribe pattern. once()
auto-unsubscribes after first firing. clearListeners() with no argument clears
all events.
vre.on('consoleLog', e => console.log('[preview]', e.level, e.message)); vre.on('error', e => console.warn('Preview error:', e.message)); vre.on('networkLog', e => console.log(e.method, e.url, e.status)); vre.on('fileChange', e => console.log(`File ${e.type}:`, e.path)); vre.once('load', e => console.log('First load:', e.entryPath));
| Event | Payload | Description |
|---|---|---|
| 'ready' | — | Service worker and engine ready |
| 'load' | { entryPath } | Preview finished loading a page |
| 'stop' | — | Preview stopped |
| 'reset' | opts | Hard reset performed |
| 'navigate' | { path } | URL changed in the preview |
| 'consoleLog' | { level, message, origin, timestamp } | Any console output |
| 'error' | { level, message, origin, timestamp } | console.error or uncaught exception |
| 'networkLog' | { url, method, status, duration, destination, timestamp } | Network request captured |
| 'domSnapshot' | { tree } | Full DOM refresh |
| 'inspectElement' | { tag, id, classList, computedStyle, boundingRect, … } | Element selected in inspector |
| 'stylePatch' | { selector, property, value } | CSS patch applied |
| 'fileChange' | { type, path } | File added, updated, deleted, or renamed |
| 'sourcesUpdate' | { sources } | Sources panel updated |
| 'snapshotRestored' | { token } | Filesystem snapshot restored |
UI / shell control
Switch the shell colour theme. Built-in values: 'dark',
'light', 'system'. Pass a token object for a custom theme.
vre.setTheme('light'); vre.setTheme('dark');
Show or hide the entire toolbar. Pass false for embed-only
mode.
vre.setToolbarVisible(false); // hide for embed mode vre.setToolbarVisible(true);
Set the devtools panel height in pixels.
vre.setDevtoolsHeight(400);
Lock the preview — hides play/stop/URL bar, disables inspector pick and
style editing. Pass true for viewer mode, false to restore
editing.
vre.setReadOnly(true); // viewer mode vre.setReadOnly(false); // restore editing
Set placeholder text in the URL bar.
vre.setTitle('My Sandbox');
Add a custom button to the toolbar. Returns a handle with a
remove() method.
const btn = vre.addToolbarButton({ icon: '<svg>...</svg>', title: 'Export', id: 'btn-export', onClick: () => console.log(vre.exportProject()) }); btn.remove();
Show a transient notification banner inside the shell. Types:
'info' (default) · 'success' · 'warn' ·
'error'. Duration in ms, default 3000. Set to 0 for no
auto-dismiss.
vre.showNotification({ message: 'Saved!', type: 'success' }); const n = vre.showNotification({ message: 'Working...', type: 'info', duration: 0 }); n.dismiss();
getIframe() returns the raw preview
<iframe> element. getRoot() returns the shell root
<div> element.
const iframe = vre.getIframe(); const root = vre.getRoot();
Temporarily places an interaction shield over the preview iframe. Useful
when a host IDE or outer shell has resize handles that may drag across the iframe. Common
cursor values: 'ew-resize', 'ns-resize', 'grabbing'.
splitter.addEventListener('mousedown', () => { vre.setIframeInteractionBlocked(true, 'ew-resize'); }); document.addEventListener('mouseup', () => { vre.setIframeInteractionBlocked(false); });
Send a raw postMessage to the preview iframe.
vre.postMessage({ type: 'myCustomEvent', data: { foo: 'bar' } });
Add or remove URL substring patterns to hide from console, network, and sources panels. Case-insensitive substring matches against the full URL.
vre.blockPatterns(['hotjar', 'segment', 'intercom']); vre.unblockPatterns('hotjar'); console.log(vre.getBlockedPatterns());
Batch helpers
Write multiple files in one shot — hot-reloads the preview once at the end instead of once per file.
vre.writeFiles([ { path: '/index.html', content: newHtml }, { path: '/style.css', content: newCss }, { path: '/app.js', content: newJs }, ], { entryPath: '/dashboard.html' });
Export the entire filesystem as a serialisable JSON object — useful for
saving and sharing. importProject() restores it, optionally running on import.
Pass { includeContent: false } to export metadata only.
const project = vre.exportProject(); // { meta: { exportedAt, entryPath, fileCount }, files, filesystem } localStorage.setItem('my-project', JSON.stringify(project)); const saved = JSON.parse(localStorage.getItem('my-project')); vre.importProject(saved, { run: true });
Complete setup example
A full working integration showing all major APIs wired together.
const vre = VRE.mount({ ownerId: 'YOUR_OWNER_ID', container: "#vre", fileSystem, entryPath: "/index.html", devtoolsOpen: true, devtoolsHeight: 280, blockPatterns: ['google-analytics', 'hotjar'], }); vre.run(); vre.on('error', e => console.warn('Preview error:', e.message)); vre.on('consoleLog', e => console.log('[preview]', e.level, e.message)); vre.on('networkLog', e => console.log('[net]', e.method, e.url, e.status)); vre.on('fileChange', e => console.log(`File ${e.type}:`, e.path)); vre.on('load', async function onFirstLoad() { vre.off('load', onFirstLoad); const ctx = await vre.getContext(); // full snapshot for LLM await vre.getDOM({ fresh: true }); const buttons = vre.queryDOM('button'); await vre.waitForElement('#btn-run', { timeout: 5000 }); await vre.click('#btn-run'); if (buttons[0]) { const el = await vre.getElement(buttons[0].path); const box = await vre.getBoxModel(buttons[0].path); console.log('Button styles:', el.computedStyle); console.log('Button box:', box); } const token = vre.snapshotFilesystem(); vre.writeFiles([{ path: '/style.css', content: newCss }]); setTimeout(() => vre.restoreSnapshot(token), 3000); setTimeout(() => vre.open('/about/index.html'), 5000); });
Runtime access control
VRE supports two access modes. Choose based on your security requirements.
| Mode | Setup | Security |
|---|---|---|
| ownerId | Simplest — just your owner ID | Authorized-domain check only |
| accessCheckUrl | Requires a backend endpoint | Strongest — private API key stays server-side, runtime performs final verification with CodeVRE |
Strict mode — runtime-token exchange flow:
// Frontend const vre = VRE.mount({ container: '#vre', fileSystem, entryPath: '/index.html', accessCheckUrl: '/api/codevre/check', }); // Express backend app.post('/api/codevre/check', async (req, res) => { const result = await fetch( 'https://us-central1-codevre-vre.cloudfunctions.net/exchangeRuntimeToken', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-codevre-api-key': process.env.CODEVRE_API_KEY, // server-only }, body: JSON.stringify({ origin: req.body.origin }), } ); res.json(await result.json()); });
Firebase Functions example:
exports.codevreCheck = onRequest( { cors: true }, async (req, res) => { if (req.method !== 'POST') { res.status(405).json({ ok: false, error: 'method-not-allowed' }); return; } const result = await fetch( 'https://us-central1-codevre-vre.cloudfunctions.net/exchangeRuntimeToken', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-codevre-api-key': process.env.CODEVRE_API_KEY, }, body: JSON.stringify({ origin: req.body?.origin }), } ); res.json(await result.json()); } );
src instead of
fileSystem + entryPath skips the VRE runtime boot and access checks. Do not use it for
secured deployments.
Authorized domains and API-key requirements:
| requireApiKey | Mode allowed | Notes |
|---|---|---|
| false | ownerId | Easiest setup — authorized-domain check only |
| true | accessCheckUrl | Requires backend endpoint and private API key |
Cross-Origin-Opener-Policy (COOP) requirements:
Cross-Origin-Opener-Policy: same-origin on your host page.
This header severs window.opener when VRE opens the external preview window, causing it to show a standalone warning instead of receiving your filesystem. Use unsafe-none (the browser default) or omit the header entirely on your app's HTML pages.
Cross-Origin-Embedder-Policy: credentialless or require-corp on your host page can also sever the opener relationship. If you need COEP for SharedArrayBuffer, apply it only to specific routes — not the catch-all /**.
Firebase Hosting — safe headers config:
// firebase.json — safe for external preview mode { "headers": [ { "source": "**", "headers": [ { "key": "Access-Control-Allow-Origin", "value": "*" }, { "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" } // ✗ Do NOT add Cross-Origin-Opener-Policy: same-origin here // ✗ Do NOT add Cross-Origin-Embedder-Policy here ] } ] }
Local dev server — safe headers:
// Node.js dev server — safe for external preview mode res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); // ✗ Do NOT set Cross-Origin-Opener-Policy: same-origin // ✗ Do NOT set Cross-Origin-Embedder-Policy on your app page
Error codes
All async methods that can fail reject with an Error with a stable
.code string.
| Code | Method(s) | Description |
|---|---|---|
| TIMEOUT | eval, waitFor, waitForElement, waitForConsole, waitForRequest, getDOM, getElement, waitForLoad | Operation did not complete within the timeout |
| EVAL_ERROR | eval, evalAsync | The evaluated code threw a runtime error |
| CAPTURE_ERROR | capture, captureElement | Screenshot failed — no canvas or SVG found |
| FILE_EXISTS | addFile | File already exists and overwrite was not set |
| NOT_FOUND | deleteFile, renameFile, diffFile, restoreSnapshot | File or snapshot not found |
try { await vre.waitForElement('.results', { timeout: 3000 }); } catch (e) { if (e.code === 'TIMEOUT') console.warn('Results never appeared'); else throw e; } try { await vre.eval('badFunction()'); } catch (e) { if (e.code === 'EVAL_ERROR') console.warn('Eval failed:', e.message); }