v0.0.7 - Disable perf probe by default (fix 322% WebProcess CPU), gate behind setting, top-frame only, quiet console logging

This commit is contained in:
Laan Tungir
2026-09-20 08:34:29 -04:00
parent d131762ce8
commit 602ee2f13b
12 changed files with 264 additions and 52 deletions
Generated
+1 -1
View File
@@ -3056,7 +3056,7 @@ dependencies = [
[[package]] [[package]]
name = "sovereign_browser" name = "sovereign_browser"
version = "0.0.5" version = "0.0.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
+1 -1
View File
@@ -6,7 +6,7 @@ members = [
[package] [package]
name = "sovereign_browser" name = "sovereign_browser"
version = "0.0.6" version = "0.0.7"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "A Linux x86 web browser built on WebKitGTK with Nostr identity" description = "A Linux x86 web browser built on WebKitGTK with Nostr identity"
+1 -1
View File
@@ -1 +1 @@
0.0.6 0.0.7
+25 -6
View File
@@ -4,10 +4,15 @@ set -e
# run.sh - Build (only if needed) and run the sovereign_browser binary. # run.sh - Build (only if needed) and run the sovereign_browser binary.
# #
# Usage: # Usage:
# ./run.sh [args...] # build if stale, then run with args # ./run.sh [args...] # build if stale (debug), then run with args
# ./run.sh --release # build/run the optimized release binary instead
# ./run.sh --build # force a rebuild, then run # ./run.sh --build # force a rebuild, then run
# ./run.sh --no-build # run without building (fails if missing) # ./run.sh --no-build # run without building (fails if missing)
# #
# The default profile is DEBUG: it rebuilds in a few seconds, which is what
# you want while iterating. Pass --release for the optimized binary (much
# slower to build, faster at runtime).
#
# The binary is rebuilt only when it is missing or when any source file # The binary is rebuilt only when it is missing or when any source file
# (src/**, Cargo.toml, Cargo.lock, www/**) is newer than the binary. # (src/**, Cargo.toml, Cargo.lock, www/**) is newer than the binary.
@@ -29,15 +34,18 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
BIN_NAME="sovereign_browser" BIN_NAME="sovereign_browser"
BIN_PATH="target/release/${BIN_NAME}"
# --- Args --------------------------------------------------------------- # --- Args ---------------------------------------------------------------
FORCE_BUILD=false FORCE_BUILD=false
NO_BUILD=false NO_BUILD=false
RELEASE=false
RUN_ARGS=() RUN_ARGS=()
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--release)
RELEASE=true
;;
--build) --build)
FORCE_BUILD=true FORCE_BUILD=true
;; ;;
@@ -50,6 +58,17 @@ for arg in "$@"; do
esac esac
done done
# --- Profile ------------------------------------------------------------
# Debug is the default; --release selects the optimized profile.
if [[ "$RELEASE" == true ]]; then
PROFILE_DIR="release"
CARGO_PROFILE_ARGS=(--release)
else
PROFILE_DIR="debug"
CARGO_PROFILE_ARGS=()
fi
BIN_PATH="target/${PROFILE_DIR}/${BIN_NAME}"
# --- Determine whether a build is needed -------------------------------- # --- Determine whether a build is needed --------------------------------
needs_build() { needs_build() {
# No binary yet -> must build. # No binary yet -> must build.
@@ -70,12 +89,12 @@ if [[ "$NO_BUILD" == true ]]; then
fi fi
print_status "Skipping build (--no-build)." print_status "Skipping build (--no-build)."
elif [[ "$FORCE_BUILD" == true ]]; then elif [[ "$FORCE_BUILD" == true ]]; then
print_status "Forcing rebuild of ${BIN_NAME} (release)..." print_status "Forcing rebuild of ${BIN_NAME} (${PROFILE_DIR})..."
cargo build --release cargo build "${CARGO_PROFILE_ARGS[@]}"
print_success "Build complete." print_success "Build complete."
elif needs_build; then elif needs_build; then
print_status "Sources changed or binary missing; building ${BIN_NAME} (release)..." print_status "Sources changed or binary missing; building ${BIN_NAME} (${PROFILE_DIR})..."
cargo build --release cargo build "${CARGO_PROFILE_ARGS[@]}"
print_success "Build complete." print_success "Build complete."
else else
print_status "Binary is up to date; skipping build." print_status "Binary is up to date; skipping build."
+171 -23
View File
@@ -62,8 +62,12 @@ fn handle_sovereign_scheme(request: &URISchemeRequest) {
let route = path.split('?').next().unwrap_or(path); let route = path.split('?').next().unwrap_or(path);
// ── nostr API ─────────────────────────────────────────────────── // ── nostr API ───────────────────────────────────────────────────
if route.starts_with("nostr/") { // NOTE: pass `path` (which still includes the query string), not
handle_nostr_api(request, &route[6..]); // `route`. The JS shim encodes the request body as `?body=<json>`,
// so stripping the query here would make every body-bearing method
// (signEvent, nip04/nip44) fail with "Invalid request body".
if path.starts_with("nostr/") {
handle_nostr_api(request, &path[6..]);
return; return;
} }
@@ -527,6 +531,16 @@ fn handle_settings_set(request: &URISchemeRequest, query: &str) {
let _ = db::db_kv_set("agent_server_enabled", new); let _ = db::db_kv_set("agent_server_enabled", new);
(new == "true", true) (new == "true", true)
} }
"perf_probe_enabled" => {
// Toggle the per-tab performance probe. OFF by default; the
// probe patches hot JS APIs and runs a permanent rAF loop.
// Takes effect on the next tab (or reload).
let mut s = settings::settings_get();
s.perf_probe_enabled = !s.perf_probe_enabled;
settings::settings_update(&serde_json::json!({"perf_probe_enabled": s.perf_probe_enabled}));
settings::settings_save();
(s.perf_probe_enabled, false)
}
_ => { _ => {
respond_error_json(request, 400, "Unknown feature"); respond_error_json(request, 400, "Unknown feature");
return; return;
@@ -687,6 +701,7 @@ fn handle_settings_config_json(request: &URISchemeRequest) {
"dev_extras": true, "dev_extras": true,
"file_access": true, "file_access": true,
"universal_access": true, "universal_access": true,
"perf_probe_enabled": s.perf_probe_enabled,
"search_engines": search_engines, "search_engines": search_engines,
"shortcuts": shortcuts, "shortcuts": shortcuts,
}); });
@@ -721,8 +736,17 @@ fn handle_nostr_api(request: &URISchemeRequest, method: &str) {
respond_error_json(request, 403, "Read-only mode"); respond_error_json(request, 403, "Read-only mode");
return; return;
} }
if let Some(ref signer) = state.signer { let signer = match &state.signer {
match serde_json::from_str::<nostr_core::types::Event>(&event_json) { Some(s) => s.clone(),
None => {
respond_error_json(request, 500, "No signer available");
return;
}
};
let pubkey_hex = state.pubkey_hex.clone();
drop(state);
match parse_unsigned_event(&event_json, &pubkey_hex) {
Ok(event) => { Ok(event) => {
match signer.sign_event(&event) { match signer.sign_event(&event) {
Ok(signed) => { Ok(signed) => {
@@ -732,10 +756,7 @@ fn handle_nostr_api(request: &URISchemeRequest, method: &str) {
Err(e) => respond_error_json(request, 500, &e.to_string()), Err(e) => respond_error_json(request, 500, &e.to_string()),
} }
} }
Err(e) => respond_error_json(request, 400, &e.to_string()), Err(e) => respond_error_json(request, 400, &e),
}
} else {
respond_error_json(request, 500, "No signer available");
} }
} }
"getRelays" => { "getRelays" => {
@@ -817,22 +838,31 @@ fn handle_nostr_api(request: &URISchemeRequest, method: &str) {
let plaintext = params.get("plaintext").and_then(|v| v.as_str()).unwrap_or(""); let plaintext = params.get("plaintext").and_then(|v| v.as_str()).unwrap_or("");
match signer.nip44_encrypt(&peer_pk, plaintext) { match signer.nip44_encrypt(&peer_pk, plaintext) {
Ok(ciphertext) => { Ok(ciphertext) => {
let json = serde_json::json!({ "result": ciphertext }); // NIP-44 ciphertext is raw bytes; the NIP-07 API
// (and the C bridge) represent it as a base64
// string. Serializing the Vec<u8> directly would
// emit a JSON array of numbers instead.
let b64 = nostr_core::util::base64_encode(&ciphertext);
let json = serde_json::json!({ "result": b64 });
respond_json(request, &serde_json::to_string(&json).unwrap_or_default()); respond_json(request, &serde_json::to_string(&json).unwrap_or_default());
} }
Err(e) => respond_error_json(request, 500, &e.to_string()), Err(e) => respond_error_json(request, 500, &e.to_string()),
} }
} }
"nip44Decrypt" => { "nip44Decrypt" => {
let ciphertext_bytes: Vec<u8> = params.get("ciphertext").map(|v| { // The JS shim sends the ciphertext as a base64 string
if let Some(s) = v.as_str() { // (NIP-07 convention). The signer expects the raw binary
s.as_bytes().to_vec() // payload, so base64-decode it. Passing the base64 text's
} else if let Some(arr) = v.as_array() { // ASCII bytes through unchanged corrupts the payload and
arr.iter().filter_map(|b| b.as_u64().map(|n| n as u8)).collect() // makes the remote signer reject it with invalid_params.
} else { let ciphertext_b64 = params.get("ciphertext").and_then(|v| v.as_str()).unwrap_or("");
Vec::new() let ciphertext_bytes = match nostr_core::util::base64_decode(ciphertext_b64) {
Ok(b) => b,
Err(e) => {
respond_error_json(request, 400, &format!("Invalid base64 ciphertext: {}", e));
return;
} }
}).unwrap_or_default(); };
match signer.nip44_decrypt(&peer_pk, &ciphertext_bytes) { match signer.nip44_decrypt(&peer_pk, &ciphertext_bytes) {
Ok(plaintext) => { Ok(plaintext) => {
let json = serde_json::json!({ "result": plaintext }); let json = serde_json::json!({ "result": plaintext });
@@ -862,8 +892,14 @@ fn extract_query_param(query: &str, key: &str) -> Option<String> {
} }
/// Simple URL percent-decoding. /// Simple URL percent-decoding.
///
/// Decodes into a byte buffer first, then interprets the result as UTF-8.
/// This is required for correctness: `encodeURIComponent` percent-encodes
/// each byte of a multi-byte UTF-8 sequence separately, so pushing each
/// decoded byte as a `char` (the previous behaviour) would corrupt any
/// non-ASCII content (e.g. emoji or accented characters in a post body).
fn url_decode(s: &str) -> String { fn url_decode(s: &str) -> String {
let mut result = String::with_capacity(s.len()); let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
let mut chars = s.chars().peekable(); let mut chars = s.chars().peekable();
while let Some(c) = chars.next() { while let Some(c) = chars.next() {
if c == '%' { if c == '%' {
@@ -871,18 +907,22 @@ fn url_decode(s: &str) -> String {
let h2 = chars.next(); let h2 = chars.next();
if let (Some(a), Some(b)) = (h1, h2) { if let (Some(a), Some(b)) = (h1, h2) {
if let Ok(byte) = u8::from_str_radix(&format!("{}{}", a, b), 16) { if let Ok(byte) = u8::from_str_radix(&format!("{}{}", a, b), 16) {
result.push(byte as char); bytes.push(byte);
continue; continue;
} }
} }
result.push(c); // Malformed escape — emit the literal '%' and the consumed chars.
bytes.push(b'%');
if let Some(a) = h1 { bytes.extend_from_slice(a.to_string().as_bytes()); }
if let Some(b) = h2 { bytes.extend_from_slice(b.to_string().as_bytes()); }
} else if c == '+' { } else if c == '+' {
result.push(' '); bytes.push(b' ');
} else { } else {
result.push(c); let mut buf = [0u8; 4];
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
} }
} }
result String::from_utf8_lossy(&bytes).into_owned()
} }
/// Parse a hex pubkey string into a PublicKey. /// Parse a hex pubkey string into a PublicKey.
@@ -895,6 +935,114 @@ fn parse_pubkey_hex(hex_str: &str) -> Option<nostr_core::types::PublicKey> {
Some(nostr_core::types::PublicKey::from_bytes(arr)) Some(nostr_core::types::PublicKey::from_bytes(arr))
} }
/// Parse an unsigned event JSON body from the JS shim into a
/// `nostr_core::types::Event`, injecting the active identity's pubkey.
///
/// The JS shim sends an *unsigned* event — typically
/// `{kind, content, tags, created_at}` with no `pubkey`/`id`/`sig`.
/// `nostr_core::types::Event` requires `pubkey` (it is not an `Option`),
/// so deserializing the raw body fails with "missing field `pubkey`".
/// This fills the pubkey in from the signed-in user, mirroring the C
/// bridge. An explicit non-empty `pubkey` in the body is preserved.
fn parse_unsigned_event(
event_json: &str,
pubkey_hex: &str,
) -> Result<nostr_core::types::Event, String> {
let mut value: serde_json::Value =
serde_json::from_str(event_json).map_err(|e| format!("Invalid JSON: {}", e))?;
if let Some(obj) = value.as_object_mut() {
let needs_pubkey = obj
.get("pubkey")
.and_then(|v| v.as_str())
.map(|s| s.is_empty())
.unwrap_or(true);
if needs_pubkey {
obj.insert(
"pubkey".to_string(),
serde_json::Value::String(pubkey_hex.to_string()),
);
}
}
serde_json::from_value::<nostr_core::types::Event>(value).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
const PK: &str = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";
#[test]
fn injects_pubkey_when_missing() {
// Exactly what the JS shim sends for a kind-1 post.
let body = r#"{"kind":1,"content":"Hello world","tags":[],"created_at":1700000000}"#;
let event = parse_unsigned_event(body, PK).expect("should parse");
assert_eq!(event.pubkey.to_hex(), PK);
assert_eq!(event.content, "Hello world");
assert_eq!(event.kind.as_u64(), 1);
assert_eq!(event.created_at, 1700000000);
}
#[test]
fn preserves_explicit_pubkey() {
let other = "0000000000000000000000000000000000000000000000000000000000000001";
let body = format!(
r#"{{"pubkey":"{}","kind":1,"content":"x","tags":[],"created_at":1}}"#,
other
);
let event = parse_unsigned_event(&body, PK).expect("should parse");
assert_eq!(event.pubkey.to_hex(), other);
}
#[test]
fn parses_tags_and_imeta() {
let body = r#"{"kind":1,"content":"hi","tags":[["imeta","url https://example.com/a.png"]],"created_at":5}"#;
let event = parse_unsigned_event(body, PK).expect("should parse");
assert_eq!(event.tags.len(), 1);
assert_eq!(event.tags[0].0[0], "imeta");
}
#[test]
fn rejects_invalid_json() {
assert!(parse_unsigned_event("not json", PK).is_err());
}
/// Round-trip the NIP-44 base64 encoding the bridge uses.
///
/// The JS shim sends the ciphertext as a base64 string; the bridge must
/// base64-decode it before handing raw bytes to the signer, and must
/// base64-encode the signer's raw output before returning it. This test
/// exercises that exact path with two real local signers.
#[test]
fn nip44_base64_round_trip() {
use nostr_signer::local::LocalSigner;
use nostr_signer::traits::NostrSigner;
let (sk_a, pk_a) = nostr_core::crypto::keys::generate_keypair();
let (sk_b, pk_b) = nostr_core::crypto::keys::generate_keypair();
let signer_a = LocalSigner::new(sk_a).expect("signer a");
let signer_b = LocalSigner::new(sk_b).expect("signer b");
let plaintext = "Hello, giftwrap!";
// Encrypt as A→B, then base64-encode (what the bridge returns).
let raw_ct = signer_a.nip44_encrypt(&pk_b, plaintext).expect("encrypt");
let b64_ct = nostr_core::util::base64_encode(&raw_ct);
// The base64 string must not be valid raw ciphertext bytes — this is
// exactly the bug: passing the ASCII of the base64 text to the signer
// fails. Decoding first must succeed.
let decoded = nostr_core::util::base64_decode(&b64_ct).expect("decode");
assert_eq!(decoded, raw_ct);
// Decrypt as B←A using the decoded raw bytes.
let decrypted = signer_b.nip44_decrypt(&pk_a, &decoded).expect("decrypt");
assert_eq!(String::from_utf8(decrypted).unwrap(), plaintext);
}
}
fn respond_json(request: &URISchemeRequest, json: &str) { fn respond_json(request: &URISchemeRequest, json: &str) {
let body = json.to_string().into_bytes(); let body = json.to_string().into_bytes();
let stream = MemoryInputStream::from_bytes(&Bytes::from_owned(body.clone())); let stream = MemoryInputStream::from_bytes(&Bytes::from_owned(body.clone()));
+6 -3
View File
@@ -188,19 +188,22 @@ pub fn nostr_inject_create_content_manager_with_probe(tab_index: i32) -> UserCon
manager.add_script(&script); manager.add_script(&script);
// Perf probe preamble (skips sovereign:// pages, sets tab index). // Perf probe preamble (skips sovereign:// pages, sets tab index).
// Top frame only — mirrors the C version (perf_probe.c:67). Injecting
// into all frames would run a separate probe instance (with its own
// rAF loop and 1s sync XHR) in every iframe.
let preamble = UserScript::new( let preamble = UserScript::new(
&perf_probe_preamble(tab_index), &perf_probe_preamble(tab_index),
UserContentInjectedFrames::AllFrames, UserContentInjectedFrames::TopFrame,
UserScriptInjectionTime::Start, UserScriptInjectionTime::Start,
&[], &[],
&[], &[],
); );
manager.add_script(&preamble); manager.add_script(&preamble);
// Perf probe body. // Perf probe body (top frame only — see above).
let probe = UserScript::new( let probe = UserScript::new(
PERF_PROBE_JS, PERF_PROBE_JS,
UserContentInjectedFrames::AllFrames, UserContentInjectedFrames::TopFrame,
UserScriptInjectionTime::Start, UserScriptInjectionTime::Start,
&[], &[],
&[], &[],
+19
View File
@@ -38,6 +38,12 @@ pub struct BrowserSettings {
pub enable_smooth_scrolling: bool, pub enable_smooth_scrolling: bool,
pub enable_accelerated_2d_canvas: bool, pub enable_accelerated_2d_canvas: bool,
pub enable_write_console_messages_to_stdout: bool, pub enable_write_console_messages_to_stdout: bool,
/// Per-tab performance probe injection. OFF by default: the probe
/// patches addEventListener/setTimeout/setInterval/XHR and runs a
/// permanent rAF loop, which adds enough main-thread overhead to
/// break scrolling on listener-heavy pages. Mirrors the C default
/// (settings.c:136).
pub perf_probe_enabled: bool,
} }
impl Default for BrowserSettings { impl Default for BrowserSettings {
@@ -69,7 +75,12 @@ impl Default for BrowserSettings {
enable_media_source: true, enable_media_source: true,
enable_smooth_scrolling: true, enable_smooth_scrolling: true,
enable_accelerated_2d_canvas: true, enable_accelerated_2d_canvas: true,
// Console-to-stdout is OFF by default: the browser's stdout is
// usually a terminal, where writes are slow and can block the
// WebProcess. Enable only when debugging.
enable_write_console_messages_to_stdout: false, enable_write_console_messages_to_stdout: false,
// Perf probe OFF by default (see field docs).
perf_probe_enabled: false,
} }
} }
} }
@@ -122,6 +133,12 @@ pub fn settings_update(json: &serde_json::Value) {
if let Some(v) = json.get("theme_dark").and_then(|v| v.as_bool()) { if let Some(v) = json.get("theme_dark").and_then(|v| v.as_bool()) {
settings.theme_dark = v; settings.theme_dark = v;
} }
if let Some(v) = json.get("perf_probe_enabled").and_then(|v| v.as_bool()) {
settings.perf_probe_enabled = v;
}
if let Some(v) = json.get("enable_write_console_messages_to_stdout").and_then(|v| v.as_bool()) {
settings.enable_write_console_messages_to_stdout = v;
}
} }
/// Load settings from the database. /// Load settings from the database.
@@ -149,6 +166,8 @@ pub fn settings_save() {
"agent_port": settings.agent_port, "agent_port": settings.agent_port,
"session_restore": settings.session_restore, "session_restore": settings.session_restore,
"theme_dark": settings.theme_dark, "theme_dark": settings.theme_dark,
"perf_probe_enabled": settings.perf_probe_enabled,
"enable_write_console_messages_to_stdout": settings.enable_write_console_messages_to_stdout,
}); });
if let Ok(json_str) = serde_json::to_string(&json) { if let Ok(json_str) = serde_json::to_string(&json) {
+10 -2
View File
@@ -218,8 +218,16 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
state.active_tab_id = tab_id; state.active_tab_id = tab_id;
drop(state); drop(state);
// Create the webview with perf probe injected (tab index baked into preamble). // Create the webview. The perf probe is injected only when explicitly
let content_manager = crate::nostr_inject::nostr_inject_create_content_manager_with_probe(tab_id); // enabled: it patches addEventListener/setTimeout/setInterval/XHR and
// runs a permanent rAF loop, which adds enough main-thread overhead to
// break scrolling on listener-heavy pages. Mirrors the C gate
// (tab_manager.c:1349). Default OFF.
let content_manager = if s.perf_probe_enabled {
crate::nostr_inject::nostr_inject_create_content_manager_with_probe(tab_id)
} else {
crate::nostr_inject::nostr_inject_create_content_manager()
};
let webview = WebView::builder() let webview = WebView::builder()
.user_content_manager(&content_manager) .user_content_manager(&content_manager)
.web_context(ctx) .web_context(ctx)
+2 -2
View File
@@ -1,7 +1,7 @@
//! Version information for sovereign_browser //! Version information for sovereign_browser
/// The current version of sovereign_browser (with leading 'v'). /// The current version of sovereign_browser (with leading 'v').
pub const VERSION: &str = "v0.0.6"; pub const VERSION: &str = "v0.0.7";
/// Major version number. /// Major version number.
pub const VERSION_MAJOR: u32 = 0; pub const VERSION_MAJOR: u32 = 0;
@@ -10,4 +10,4 @@ pub const VERSION_MAJOR: u32 = 0;
pub const VERSION_MINOR: u32 = 0; pub const VERSION_MINOR: u32 = 0;
/// Patch version number. /// Patch version number.
pub const VERSION_PATCH: u32 = 6; pub const VERSION_PATCH: u32 = 7;
+7 -4
View File
@@ -50,8 +50,11 @@ pub fn web_context_configure_settings(settings: &webkit2gtk::Settings) {
// Deprecated since 2.32 with no replacement; keep for explicitness. // Deprecated since 2.32 with no replacement; keep for explicitness.
#[allow(deprecated)] #[allow(deprecated)]
settings.set_enable_accelerated_2d_canvas(true); settings.set_enable_accelerated_2d_canvas(true);
// Enable console messages to stdout so we can debug window.nostr injection // Console-to-stdout is OFF by default. The browser's stdout is usually a
// and page-level issues. The C version connects to console-message signal; // terminal, where writes are slow and can block the WebProcess; on a
// this is simpler for now. // chatty page this is a real CPU cost. Enable via the
settings.set_enable_write_console_messages_to_stdout(true); // `enable_write_console_messages_to_stdout` setting when debugging.
settings.set_enable_write_console_messages_to_stdout(
crate::settings::settings_get().enable_write_console_messages_to_stdout,
);
} }
+11
View File
@@ -145,6 +145,17 @@
onclick="toggle('universal_access')"></div> onclick="toggle('universal_access')"></div>
</div> </div>
<div class="setting">
<div>
<div class="setting-name">Performance Probe</div>
<div class="setting-desc">Per-tab CPU/FPS instrumentation. Adds main-thread
overhead (patches timers, listeners, XHR; runs a rAF loop). Off by
default. Takes effect on new tabs.</div>
</div>
<div class="toggle" data-feature="perf_probe_enabled"
onclick="toggle('perf_probe_enabled')"></div>
</div>
<div class="setting"> <div class="setting">
<div><div class="setting-name">CORS Enforcement</div></div> <div><div class="setting-name">CORS Enforcement</div></div>
<div class="toggle off" style="opacity:0.5"></div> <div class="toggle off" style="opacity:0.5"></div>
+2 -1
View File
@@ -49,7 +49,8 @@ function applyConfig(d) {
agent_server_enabled: d.agent_server_enabled, agent_server_enabled: d.agent_server_enabled,
dev_extras: d.dev_extras, dev_extras: d.dev_extras,
file_access: d.file_access, file_access: d.file_access,
universal_access: d.universal_access universal_access: d.universal_access,
perf_probe_enabled: d.perf_probe_enabled
}; };
document.querySelectorAll('.toggle[data-feature]').forEach(function(el) { document.querySelectorAll('.toggle[data-feature]').forEach(function(el) {
var key = el.getAttribute('data-feature'); var key = el.getAttribute('data-feature');