From 7936d0c2a3a9294dd8e2a56131888d9e0df20682 Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 15:08:04 +0700 Subject: [PATCH 1/3] Read Ollama account email from settings header only --- CHANGELOG.md | 1 + rust/src/providers/ollama/mod.rs | 49 ++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72ce7fc111..22f2a35219 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ## [Windows] Unreleased +- Ollama: the account email is read only from the settings page header (`id="header-email"`) instead of the first address anywhere on the page, so a footer or support address is no longer shown as the account; a page without that header shows no email (upstream v0.75.0). - Settings providers: show the FLORA Workspace ID and Fireworks account slug fields, which were defined but never displayed, so they no longer need the `FLORA_WORKSPACE_ID` / `FIREWORKS_ACCOUNT_SLUG` environment variables. - OpenAI API: each Admin refresh also reads the last hour of 1-minute usage from the Admin Usage API (input plus output tokens, cached input included, Batch API traffic kept out of limit headroom) and carries a Tokens/min grid with a reporting frontier, so buckets that arrive late show as pending instead of zero. The backend can also load 15-minute, 1-hour or 6-hour windows, read per-project rate-limit caps, and measure headroom against manual per-model organization limits stored in settings. The per-refresh request can be turned off, and a failure never affects the daily usage. The menu bar token comes in a later release (upstream #4495). - OpenAI API: the card shows the last-hour Tokens/min grid (latest, peak, 5-minute average, reporting frontier, and pending minutes drawn as outlines), and Settings > Providers > OpenAI adds a Live Usage section with a refresh switch, a per-model organization limits editor, a 15-minute / 1-hour / 6-hour window with per-model rows, and the organization caps list, which stays hidden when a project id is saved (upstream #4495). diff --git a/rust/src/providers/ollama/mod.rs b/rust/src/providers/ollama/mod.rs index 5b8ca218dd..21a327ddf2 100755 --- a/rust/src/providers/ollama/mod.rs +++ b/rust/src/providers/ollama/mod.rs @@ -479,10 +479,14 @@ impl OllamaProvider { None } - /// Parse account email from the page + /// Parse the account email from the settings page header (`id="header-email"`). + /// + /// Only that element is read, matching upstream CodexBar: a page-wide scan would + /// pick up unrelated addresses (footer/support links). A missing header yields no email. fn parse_account_email(&self, html: &str) -> Option { - let re = Regex::new(r#"[\w.+-]+@[\w-]+\.[\w.-]+"#).ok()?; - re.find(html).map(|m| m.as_str().to_string()) + let re = Regex::new(r#"id="header-email"[^>]*>([^<]+)<"#).ok()?; + let raw = re.captures(html)?.get(1)?.as_str().trim(); + raw.contains('@').then(|| raw.to_string()) } } @@ -1244,6 +1248,45 @@ mod tests { ); } + #[test] + fn account_email_comes_from_header_not_unrelated_page_addresses() { + // A footer/support address that precedes the header must not be picked. + let provider = OllamaProvider::new(); + let html = format!( + r#" +

user@example.com

{CREDIT_WALLET_FIXTURE}"# + ); + assert_eq!( + provider.parse_account_email(&html).as_deref(), + Some("user@example.com") + ); + } + + #[test] + fn account_email_is_absent_without_header_even_if_page_has_addresses() { + // No header-email element: no account email, and no page-wide fallback to the footer address. + let provider = OllamaProvider::new(); + let html = format!( + r#"{CREDIT_WALLET_FIXTURE}"# + ); + assert_eq!(provider.parse_account_email(&html), None); + + let result = provider + .parse_usage_result(&html) + .expect("wallet page without a header still parses"); + assert_eq!(result.usage.account_email, None); + assert_eq!(result.usage.login_method.as_deref(), Some("pro")); + } + + #[test] + fn account_email_header_without_at_sign_is_ignored() { + let provider = OllamaProvider::new(); + assert_eq!( + provider.parse_account_email(r#"

Signed in

"#), + None + ); + } + #[test] fn credit_wallet_monthly_spending_cannot_become_missing_balance() { let provider = OllamaProvider::new(); From a65036de17a8124196569cd472718047248aa27b Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 15:15:00 +0700 Subject: [PATCH 2/3] Match only a real id attribute for the Ollama header email --- rust/src/providers/ollama/mod.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/rust/src/providers/ollama/mod.rs b/rust/src/providers/ollama/mod.rs index 21a327ddf2..4a5b090fde 100755 --- a/rust/src/providers/ollama/mod.rs +++ b/rust/src/providers/ollama/mod.rs @@ -484,7 +484,8 @@ impl OllamaProvider { /// Only that element is read, matching upstream CodexBar: a page-wide scan would /// pick up unrelated addresses (footer/support links). A missing header yields no email. fn parse_account_email(&self, html: &str) -> Option { - let re = Regex::new(r#"id="header-email"[^>]*>([^<]+)<"#).ok()?; + // Leading whitespace anchors a real `id` attribute so `data-id="header-email"` cannot match. + let re = Regex::new(r#"\sid="header-email"[^>]*>([^<]+)<"#).ok()?; let raw = re.captures(html)?.get(1)?.as_str().trim(); raw.contains('@').then(|| raw.to_string()) } @@ -1287,6 +1288,23 @@ mod tests { ); } + #[test] + fn account_email_ignores_data_id_lookalike() { + let provider = OllamaProvider::new(); + assert_eq!( + provider.parse_account_email( + r#"other@example.com

user@example.com

"# + ) + .as_deref(), + Some("user@example.com") + ); + assert_eq!( + provider + .parse_account_email(r#"other@example.com"#), + None + ); + } + #[test] fn credit_wallet_monthly_spending_cannot_become_missing_balance() { let provider = OllamaProvider::new(); From 491902754cf73507bb896530a62df8a87dedd5ec Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Fri, 16 Oct 2026 15:39:19 +0700 Subject: [PATCH 3/3] Parse Ollama header email from real start-tag attributes --- rust/src/providers/ollama/mod.rs | 33 +++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/rust/src/providers/ollama/mod.rs b/rust/src/providers/ollama/mod.rs index 4a5b090fde..e7ff5f3932 100755 --- a/rust/src/providers/ollama/mod.rs +++ b/rust/src/providers/ollama/mod.rs @@ -484,9 +484,22 @@ impl OllamaProvider { /// Only that element is read, matching upstream CodexBar: a page-wide scan would /// pick up unrelated addresses (footer/support links). A missing header yields no email. fn parse_account_email(&self, html: &str) -> Option { - // Leading whitespace anchors a real `id` attribute so `data-id="header-email"` cannot match. - let re = Regex::new(r#"\sid="header-email"[^>]*>([^<]+)<"#).ok()?; - let raw = re.captures(html)?.get(1)?.as_str().trim(); + // Walk start tags attribute by attribute: a quoted value is consumed whole, so text + // such as `data-x=' id="header-email"'` or `data-id="header-email"` cannot match. + let tag_re = Regex::new( + r#"<[A-Za-z][A-Za-z0-9:-]*((?:\s+[^\s=>/"']+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>"']+))?)*)\s*/?>([^<]*)"#, + ) + .ok()?; + let attr_re = + Regex::new(r#"([^\s=>/"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"']+)))?"#).ok()?; + let raw = tag_re.captures_iter(html).find_map(|tag| { + let is_header = attr_re.captures_iter(tag.get(1)?.as_str()).any(|attr| { + let value = attr.get(2).or(attr.get(3)).or(attr.get(4)); + attr[1].eq_ignore_ascii_case("id") + && value.is_some_and(|v| v.as_str() == "header-email") + }); + is_header.then(|| tag.get(2).map_or("", |m| m.as_str()).trim()) + })?; raw.contains('@').then(|| raw.to_string()) } } @@ -1303,6 +1316,20 @@ mod tests { .parse_account_email(r#"other@example.com"#), None ); + assert_eq!( + provider.parse_account_email( + r#"other@example.com"# + ), + None + ); + assert_eq!( + provider + .parse_account_email( + r#"other@example.com

user@example.com

"# + ) + .as_deref(), + Some("user@example.com") + ); } #[test]