From 72562d7b320f5ba7c14b6193e9eaebc7f01d21ac Mon Sep 17 00:00:00 2001 From: argszero Date: Fri, 25 Sep 2026 11:46:38 +0800 Subject: [PATCH] fix(ui): let the ops key rows be named the way their own card names them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ops runtime card renders one line per provider with `T("ops.keys.count", { total, on })`, and `total` is the backend's unfiltered `COUNT(*)` over the `keys` table — every status, including `paused` and `off` (soft-deleted). The stat card directly above it counts `WHERE status = 'on'`, and that is what the product means by "listed": the sharing page binds its "Listed" pill to `status === 'on'`, and a count that includes non-`on` rows is called "history" there. So the row said "6 listed keys · 5 enabled" next to a card saying "Active keys: 5" — one word, one viewport, two numbers. The card already names these rows "upstream key" in its own title ("Upstream key status") and in its empty state ("No upstream keys listed yet"); only the count line and the subtitle drifted. Provenance is drift, not a trade-off: the three pack keys were authored in 09e4121, which renamed the prototype's wording, while the data source from `85982e8` was untouched. Fix the WORD, not the number. `total` must keep counting every status — otherwise the row's own pill (`off >= total` -> "All disabled") becomes structurally always green, which deletes the promise instead of keeping it. No new i18n keys (the key set is unchanged), no backend change; the subtitle, which names the same number, moves with the count line, and the static `data-i18n` fallback and the `i18n.js` cache-bust token move with the pack. Gate `state_gate::the_ops_key_rows_are_named_the_way_their_own_card_names_them` derives the noun from `ops.keys.title` (no wording is written into the gate, so renaming the card does not need a gate edit), and `ui/README.md` states the contract, the gate's scope and one leg declared blind to it. Co-authored-by: argszero --- src/state_gate.rs | 369 ++++++++++++++++++++++++++++++++++++++++++++++ ui/README.md | 28 ++++ ui/index.html | 4 +- ui/js/i18n.js | 15 +- 4 files changed, 410 insertions(+), 6 deletions(-) diff --git a/src/state_gate.rs b/src/state_gate.rs index 6a5bad4..5cea6b1 100644 --- a/src/state_gate.rs +++ b/src/state_gate.rs @@ -16311,3 +16311,372 @@ fn r163_variant_self_call(app: &str) -> String { fn r163_variant_defect(app: &str) -> String { r163_variant_drop_fallback(&r163_variant_refusal_falls_through(app)) } + +// ── C2174:运营「上游 key 状态」卡的行计数必须用这张卡自己的名词 ──────────────────────────────── + +/// 这一卡的三个键。**标题是名词的唯一来源**(见 [`ops_key_rows_noun`])。 +const OPS_KEYS_TITLE: &str = "ops.keys.title"; +const OPS_KEYS_COUNT: &str = "ops.keys.count"; +const OPS_KEYS_SUB: &str = "ops.keys.sub"; + +/// 卡片自己给这批行的**名词**:`ops.keys.title` 去掉最后一个空格分隔的 token。 +/// +/// zh「上游 key 状态」→「上游 key」;en「Upstream key status」→「Upstream key」。 +/// 门禁里**零手写**这些词 —— 改称谓只改语言包,不改门禁(否则改一次词就要改一次门禁)。 +/// 标题只剩一个 token ⇒ `None`(「去掉末词」不再是可靠的派生,而门禁宁可判前置失败)。 +fn ops_key_rows_noun(title: &str) -> Option { + let toks: Vec<&str> = title.split_whitespace().collect(); + if toks.len() < 2 { + return None; + } + let noun = toks[..toks.len() - 1].join(" "); + if noun.is_empty() { + None + } else { + Some(noun) + } +} + +/// `haystack` 是否含 `noun`(**ASCII 大小写不敏感** —— en 的「Upstream key」要认「upstream keys」; +/// CJK 不受影响)。 +fn ops_noun_mentioned(haystack: &str, noun: &str) -> bool { + haystack + .to_ascii_lowercase() + .contains(&noun.to_ascii_lowercase()) +} + +/// 语言包区段里**真的当过键**的 `ops.keys.*` 名字(只认 `"key": ` 这种形状 —— +/// 注释里提到的键名不算,见坑 #296 家族)。 +fn ops_keys_in(region: &str) -> BTreeSet { + // 先剥注释:语言包区段里现在有解释性散文(本卡的三行 `//` 就在其中),而注释里写下的 + // `"ops.keys.x": ` 不是键(坑 #296 家族 —— 注释里的表达式会把计数带偏)。 + let region = code_text(region); + let region = region.as_str(); + let mut out = BTreeSet::new(); + let mut from = 0usize; + while let Some(rel) = region[from..].find("\"ops.keys.") { + let at = from + rel + 1; + let rest = ®ion[at..]; + let end = match rest.find('"') { + Some(e) => e, + None => break, + }; + if rest[end + 1..].trim_start().starts_with(':') { + out.insert(rest[..end].to_string()); + } + from = at + end + 1; + } + out +} + +/// 一个语言包在这一卡上的取值与派生名词。 +struct R2174Pack { + title: Option, + noun: Option, + count: Option, + sub: Option, + keys: BTreeSet, +} + +fn r2174_pack(region: &str) -> R2174Pack { + let title = pack_string(region, OPS_KEYS_TITLE); + let noun = title.as_deref().and_then(ops_key_rows_noun); + R2174Pack { + title, + noun, + count: pack_string(region, OPS_KEYS_COUNT), + sub: pack_string(region, OPS_KEYS_SUB), + keys: ops_keys_in(region), + } +} + +/// 一次读取的全部证据与四条判词(R1 的产物是两包的 `noun`,不是判词)。 +struct R2174Reading { + zh: R2174Pack, + en: R2174Pack, + r2: bool, + r3: bool, + r4: bool, + r5: bool, +} + +impl R2174Reading { + fn report(&self) -> String { + format!( + "r2={} r3={} r4={} r5={} | zh noun={:?} count={:?} sub={:?} | en noun={:?} count={:?} sub={:?}", + self.r2, + self.r3, + self.r4, + self.r5, + self.zh.noun, + self.zh.count, + self.zh.sub, + self.en.noun, + self.en.count, + self.en.sub + ) + } +} + +/// 读三份制品(语言包 / markup / 渲染器),导出四条判词。名册与期望值全部**派生**: +/// 名词来自标题、键集来自语言包、渲染器与 markup 的存在性来自那两个文件本身。 +fn r2174_read(pack: &str, html: &str, app: &str) -> R2174Reading { + let zh = r2174_pack(pack_region_strict(pack, ZH_PACK_START, EN_PACK_START)); + let en = r2174_pack(pack_region_strict(pack, EN_PACK_START, PACK_END)); + + let named = |p: &R2174Pack, v: &Option| match (p.noun.as_deref(), v.as_deref()) { + (Some(n), Some(text)) => ops_noun_mentioned(text, n), + _ => false, + }; + + // R4(前置,防空集/空语料假绿 —— 坑 #68 家族):两包的三个串都取到、名词派生成功且**确实 + // 短了**、两包的 `ops.keys.*` 键集相等且非空、三份语料各自携带这张卡的一部分。 + let r4 = zh.title.is_some() + && zh.count.is_some() + && zh.sub.is_some() + && en.title.is_some() + && en.count.is_some() + && en.sub.is_some() + && zh.noun.is_some() + && en.noun.is_some() + && zh.noun != zh.title + && en.noun != en.title + && !zh.keys.is_empty() + && zh.keys == en.keys + && html.contains(&format!("data-i18n=\"{OPS_KEYS_TITLE}\"")) + && app.contains(&format!("T(\"{OPS_KEYS_COUNT}\"")); + + // R5(阳性对照):计数行必须**仍**带两个占位符 —— 挡住「把计数行删掉 / 删掉一个数」那种 + // 「把承诺删掉」的化妆式修法(同 C2172 的 R5)。 + let r5 = zh + .count + .as_deref() + .is_some_and(|v| v.contains("{total}") && v.contains("{on}")) + && en + .count + .as_deref() + .is_some_and(|v| v.contains("{total}") && v.contains("{on}")); + + R2174Reading { + r2: named(&zh, &zh.count) && named(&en, &en.count), + r3: named(&zh, &zh.sub) && named(&en, &en.sub), + r4, + r5, + zh, + en, + } +} + +/// 轴:运营「上游 key 状态」卡的行计数说的是它数的那批行。 +/// +/// `/api/ops/runtime` 的 `key_health` 每行给 `{provider,total,on,off}`,其中 `total` 是 +/// `COUNT(*)`(**全部状态**:`on` / `paused` / `off`=软删),而同屏正上方的统计卡 +/// (`ops.stats.keys`「上架 key 数」)数的是 `WHERE status = 'on'`。产品把「上架」严格绑给 +/// `status='on'`(共享页「上架中」;含非 `on` 行的计数在那里叫「历史」)⇒ 这张卡的行若把 +/// `total` 称作「上架 key」,同一个词在同一个视口里就带了两个数。 +/// +/// 三条规则(R2/R3/R5)各有各的牙,见 [`the_r2174_rules_have_teeth`]。 +#[test] +fn the_ops_key_rows_are_named_the_way_their_own_card_names_them() { + let rd = r2174_read(I18N_JS, INDEX_HTML, APP_JS); + assert!( + rd.r4, + "前置不成立 —— 门禁的语料或键集已失真(空集上的断言会假绿):{}", + rd.report() + ); + assert!( + rd.r2, + "运营「上游 key 状态」卡的行计数没有用这张卡自己的名词 —— 它数的是**全部状态**的上游 key\ + (含 `paused` / `off`),而「上架 key」= `status='on'`(同屏「上架 key 数」那张卡与共享页\ + 「上架中」用的就是后者):{}", + rd.report() + ); + assert!( + rd.r3, + "同一张卡的副标题(`{OPS_KEYS_SUB}`)给的是**同一个数**的另一个称呼,也必须用卡片自己的\ + 名词 —— 只修计数行会在副标题留下同一处错词:{}", + rd.report() + ); + assert!( + rd.r5, + "计数行丢了占位符(`{{total}}` / `{{on}}`)—— 「把承诺删掉」不是修:{}", + rd.report() + ); +} + +/// 三条规则的**牙**:每个合成变异体只打翻它针对的那一条(基线=已知为绿的活树)。 +/// +/// ⚠️ 变异体一律从**活树**构造,每个锚点先断言唯一(#612 家族):变体没生效时构造器先响, +/// 而不是让某条腿因错误的原因变绿(#605)。 +#[test] +fn the_r2174_rules_have_teeth() { + let live = r2174_read(I18N_JS, INDEX_HTML, APP_JS); + assert!( + live.r2 && live.r3 && live.r4 && live.r5, + "基线不是绿的 —— 变异体的读数无从解释:{}", + live.report() + ); + + // ① 计数行退回缺陷措辞(两个包一起退,那是缺陷的忠实形状)⇒ 只翻 R2。 + let v = r2174_variant_count_reverts(I18N_JS); + let rd = r2174_read(&v, INDEX_HTML, APP_JS); + assert_eq!( + (rd.r2, rd.r3, rd.r5), + (false, true, true), + "① 计数行的名词退回「上架家族」只应翻掉 R2:{}", + rd.report() + ); + + // ② 只把副标题退回缺陷措辞 ⇒ 只翻 R3。 + let v = r2174_variant_sub_reverts(I18N_JS); + let rd = r2174_read(&v, INDEX_HTML, APP_JS); + assert_eq!( + (rd.r2, rd.r3, rd.r5), + (true, false, true), + "② 副标题的名词退回「上架家族」只应翻掉 R3:{}", + rd.report() + ); + + // ③ 计数行丢一个占位符 ⇒ 只翻 R5。 + let v = r2174_variant_placeholder_dropped(I18N_JS); + let rd = r2174_read(&v, INDEX_HTML, APP_JS); + assert_eq!( + (rd.r2, rd.r3, rd.r5), + (true, true, false), + "③ 丢掉 `{{on}}` 只应翻掉 R5:{}", + rd.report() + ); + + // ④ **声明为盲**的一条腿:把**称谓**整体改成与数据不符的那一族 —— 标题 + 计数行 + 副标题 + // 一起改、彼此自洽 ⇒ 门禁**全绿**。这条词法规则挡不住「改名而不改数据」的竞争修法 + // (挡它的是设计基线原型写的就是「上游 key」,以及 jsdom 探针的 `m_card_widen` 腿)。 + // 这里断言它**确实**全绿 —— 盲区要被测量,不能被当成「反正不会发生」。 + let v = r2174_variant_coherent_wrong_rename(I18N_JS); + let rd = r2174_read(&v, INDEX_HTML, APP_JS); + assert!( + rd.r2 && rd.r3 && rd.r4 && rd.r5, + "④ 是**声明为盲**的腿:它必须真的全绿(否则它就不是盲区,而是我们的判别式漏了它):{}", + rd.report() + ); +} + +/// 提取器自证:名词派生与键扫描在**合成输入**上按声明工作(与活树无关)。 +#[test] +fn the_r2174_scanners_have_teeth() { + // 名词 = 标题去掉末词(两种语言的形状)。 + assert_eq!( + ops_key_rows_noun("上游 key 状态").as_deref(), + Some("上游 key") + ); + assert_eq!( + ops_key_rows_noun("Upstream key status").as_deref(), + Some("Upstream key") + ); + // 单 token ⇒ None(派生不可靠,宁判前置失败)。 + assert_eq!(ops_key_rows_noun("状态"), None); + assert_eq!(ops_key_rows_noun(""), None); + // 多余空白不改变结果(split_whitespace)。 + assert_eq!( + ops_key_rows_noun("上游 key 状态").as_deref(), + Some("上游 key") + ); + + // 大小写:en 的复数要认单数名词;zh 原样。 + assert!(ops_noun_mentioned( + "Total upstream keys here", + "Upstream key" + )); + assert!(ops_noun_mentioned( + "{total} 个上游 key · {on} 个启用", + "上游 key" + )); + assert!(!ops_noun_mentioned("{total} listed keys", "upstream key")); + + // 键扫描只认 `"key": ` 形状:注释里提到的键名不算。 + let synthetic = " // \"ops.keys.fake\": 这是注释\n \"ops.keys.count\": \"x\",\n"; + let keys = ops_keys_in(synthetic); + assert!( + keys.contains("ops.keys.count") && !keys.contains("ops.keys.fake"), + "键扫描把注释里的键名当成了真键:{keys:?}" + ); +} + +/// 变异体①:计数行退回缺陷措辞(`total` 被称作「上架 key」)。 +fn r2174_variant_count_reverts(pack: &str) -> String { + let zh = " \"ops.keys.count\": \"{total} 个上游 key · {on} 个启用\",\n"; + let en = " \"ops.keys.count\": \"{total} upstream keys · {on} enabled\",\n"; + assert_eq!(pack.matches(zh).count(), 1, "zh 计数行锚点不唯一"); + assert_eq!(pack.matches(en).count(), 1, "en 计数行锚点不唯一"); + pack.replace( + zh, + " \"ops.keys.count\": \"{total} 个上架 key · {on} 个启用\",\n", + ) + .replace( + en, + " \"ops.keys.count\": \"{total} listed keys · {on} enabled\",\n", + ) +} + +/// 变异体②:只把副标题退回缺陷措辞(计数行不动)。 +fn r2174_variant_sub_reverts(pack: &str) -> String { + let zh = " \"ops.keys.sub\": \"按厂商聚合 · 启用状态与总上游 key 数\",\n"; + let en = + " \"ops.keys.sub\": \"Aggregated by provider · enabled vs. total upstream keys\",\n"; + assert_eq!(pack.matches(zh).count(), 1, "zh 副标题锚点不唯一"); + assert_eq!(pack.matches(en).count(), 1, "en 副标题锚点不唯一"); + pack.replace( + zh, + " \"ops.keys.sub\": \"按厂商聚合 · 启用状态与总上架数\",\n", + ) + .replace( + en, + " \"ops.keys.sub\": \"Aggregated by provider · enabled vs. total listings\",\n", + ) +} + +/// 变异体③:计数行丢掉 `{on}` 占位符(词没变)⇒ 只应翻 R5。 +fn r2174_variant_placeholder_dropped(pack: &str) -> String { + let zh = " \"ops.keys.count\": \"{total} 个上游 key · {on} 个启用\",\n"; + assert_eq!(pack.matches(zh).count(), 1, "zh 计数行锚点不唯一"); + pack.replace(zh, " \"ops.keys.count\": \"{total} 个上游 key\",\n") +} + +/// 变异体④(**声明为盲**的腿):把称谓整体改成与数据不符的那一族 —— 标题、计数行、副标题 +/// 一起改且彼此自洽 ⇒ 门禁全绿。(与探针的 `m_card_widen` 腿配对:那条腿在运行期拒绝它。) +fn r2174_variant_coherent_wrong_rename(pack: &str) -> String { + let anchors = [ + " \"ops.keys.title\": \"上游 key 状态\",\n", + " \"ops.keys.title\": \"Upstream key status\",\n", + " \"ops.keys.count\": \"{total} 个上游 key · {on} 个启用\",\n", + " \"ops.keys.count\": \"{total} upstream keys · {on} enabled\",\n", + " \"ops.keys.sub\": \"按厂商聚合 · 启用状态与总上游 key 数\",\n", + " \"ops.keys.sub\": \"Aggregated by provider · enabled vs. total upstream keys\",\n", + ]; + for a in anchors { + assert_eq!(pack.matches(a).count(), 1, "变异体④锚点不唯一:{a:?}"); + } + pack.replace( + " \"ops.keys.title\": \"上游 key 状态\",\n", + " \"ops.keys.title\": \"上架 key 状态\",\n", + ) + .replace( + " \"ops.keys.title\": \"Upstream key status\",\n", + " \"ops.keys.title\": \"Listed key status\",\n", + ) + .replace( + " \"ops.keys.count\": \"{total} 个上游 key · {on} 个启用\",\n", + " \"ops.keys.count\": \"{total} 个上架 key · {on} 个启用\",\n", + ) + .replace( + " \"ops.keys.count\": \"{total} upstream keys · {on} enabled\",\n", + " \"ops.keys.count\": \"{total} listed keys · {on} enabled\",\n", + ) + .replace( + " \"ops.keys.sub\": \"按厂商聚合 · 启用状态与总上游 key 数\",\n", + " \"ops.keys.sub\": \"按厂商聚合 · 启用状态与总上架 key 数\",\n", + ) + .replace( + " \"ops.keys.sub\": \"Aggregated by provider · enabled vs. total upstream keys\",\n", + " \"ops.keys.sub\": \"Aggregated by provider · enabled vs. total listed keys\",\n", + ) +} diff --git a/ui/README.md b/ui/README.md index 89a1351..85571d6 100644 --- a/ui/README.md +++ b/ui/README.md @@ -677,6 +677,34 @@ try { const w = await api.get("/api/wallet"); if (w) D.USER.balance = w.balance; - **CI 覆盖**:`src/state_gate.rs::the_ops_members_balance_cell_is_the_half_its_caption_names`(三规则:① 取值表达式按**标识符 token** 同时读到 `balance` 与 `gift_balance` ② 两包列头须落在**可花族**(标记由 `wallet.balance` 的包值派生)③ 反向:`admin.emp.col.perm` 仍只读 `balance`)。 - ⚠️ **射程**:门禁是**词法**的 —— 它证明取值**读到**两个字段、名字落在可花族,**不证明算术是 `+`**;行为由仪器 `c2173_probe.js` 钉(19 腿;甲/乙两棵合法树全绿,三条「半个答案 / 化妆 / 自相矛盾」被拒)。 +## 运营「上游 key 状态」卡的行计数用**这张卡自己的名词**(C2174) + +- **一个词一个数**:`/api/ops/runtime` 的 `key_health` 每行是 `{provider,total,on,off}`,其中 + `total` 是 `COUNT(*)`(**全部状态**:`on` / `paused` / `off`=软删/彻底下架)。而产品把 + 「上架」严格绑给 `status='on'` —— 同屏正上方那张卡 `ops.stats.keys`(zh「上架 key 数」) + 数的是 `WHERE status='on'`,共享页也把「上架中」绑给 `status==='on'`、把含非 `on` 行的计数 + 叫「历史」(`cnt.hist`)。于是这一行若把 `total` 称作「上架 key」,同一个词在同一个视口里 + 就带了两个数(6 行/5 启用 vs 上面那张卡的 5)。 +- **正确称谓在卡片自己身上**:这张卡的**标题**(`ops.keys.title` zh「上游 key 状态」)与**空态** + (`ops.keys.empty`「尚未上架任何**上游 key**」)称这批行为「上游 key」;设计基线原型写的是 + `k.n + " 个上游 key"`。漂移发生在改版那一次(`09e4121` / #157 新造这组键时把「上游」改称 + 「上架」),数据源(`COUNT(*)`,`85982e8` / #80)一字未改。 +- **修法**:**改词不改数** —— `ops.keys.count` 与 `ops.keys.sub` 两包四串换回「上游 key / + upstream key」族 + `ui/index.html` 的 `data-i18n="ops.keys.sub"` 静态 fallback 同步 + + `i18n.js?v=` 自增。**零新 i18n 键、零后端改动**。⛔ 不许把 `total` 改成只数 `on` + (同一行的 pill「N 个停用 / 全部停用」会结构性恒绿=把承诺删掉),也不许把同屏那张 + 「上架 key 数」改宽(它的值确实只有 `on`)。 +- **CI 覆盖**:`src/state_gate.rs::the_ops_key_rows_are_named_the_way_their_own_card_names_them` + (R1 名词=`ops.keys.title` 去掉末 token,**门禁里零手写词**;R2 计数行两包都含该名词; + R3 同卡副标题也必须含(它给的是同一个数的另一个称呼);R4 前置:两包三串齐、名词确实变短、 + 两包 `ops.keys.*` 键集相等、三份语料各带这张卡的一部分;R5 阳性对照:两个占位符仍在)。 + 配 `the_r2174_scanners_have_teeth`(名词派生 / 大小写 / **注释里的键名不算键**)。 +- ⚠️ **射程与一条声明为盲的腿**:门禁是**词法**的 —— 它证「行计数的名词与卡片自己的标题同源」, + **不证**屏幕上那一刻的文本(那半归 jsdom 探针),也**不证** `total` 的算法该不该含 `off` 行 + (那由 pill 的结构需求固定)。**盲区(由 `the_r2174_rules_have_teeth` 的变异体④实测)**: + 把**称谓整体**改成与数据不符的那一族(标题 + 计数行 + 副标题一起改、彼此自洽)门禁**全绿** —— + 挡它的是设计基线(原型写的就是「上游 key」)与运行期探针,不是这条词法规则。 + ## 表单控件的 property 式 `disabled`:必须有一个在**回收路径**上的清除点(R139) diff --git a/ui/index.html b/ui/index.html index 7743375..9a1ed27 100644 --- a/ui/index.html +++ b/ui/index.html @@ -784,7 +784,7 @@

今日调用量(按小时)

上游 key 状态

-

按厂商聚合 · 启用状态与总上架数

+

按厂商聚合 · 启用状态与总上游 key 数

@@ -861,7 +861,7 @@

使用模型

- + diff --git a/ui/js/i18n.js b/ui/js/i18n.js index cfc8315..941599a 100644 --- a/ui/js/i18n.js +++ b/ui/js/i18n.js @@ -660,9 +660,13 @@ "ops.hours.title": "今日调用量(按小时)", "ops.hours.sub": "按小时用量记录 · 服务端 UTC 时区", "ops.hours.empty": "今日暂无调用记录", + // 这一卡数的是**全部状态**的上游 key(含 paused / off),所以行计数必须用卡片自己的称谓 + // 「上游 key」;「上架 key」= status='on'(同屏「上架 key 数」那张卡与共享页「上架中」的画)。 + // 静态门禁 state_gate::the_ops_key_rows_are_named_the_way_their_own_card_names_them 从 + // ops.keys.title 派生名词,标题 / count / sub 三处必须同源。 "ops.keys.title": "上游 key 状态", - "ops.keys.sub": "按厂商聚合 · 启用状态与总上架数", - "ops.keys.count": "{total} 个上架 key · {on} 个启用", + "ops.keys.sub": "按厂商聚合 · 启用状态与总上游 key 数", + "ops.keys.count": "{total} 个上游 key · {on} 个启用", "ops.keys.allOn": "全部启用", "ops.keys.someOff": "{n} 个停用", "ops.keys.allOff": "全部停用", @@ -1476,9 +1480,12 @@ "ops.hours.title": "Calls today (by hour)", "ops.hours.sub": "Hourly usage records · server UTC", "ops.hours.empty": "No calls recorded today", + // The row counts upstream keys of **every** status (including paused / off), so it must use the + // card's own noun ("upstream key"); "listed key" means status = 'on' — the wording of the + // "Active keys" stat card and of the sharing page's "Listed" pill. "ops.keys.title": "Upstream key status", - "ops.keys.sub": "Aggregated by provider · enabled vs. total listings", - "ops.keys.count": "{total} listed keys · {on} enabled", + "ops.keys.sub": "Aggregated by provider · enabled vs. total upstream keys", + "ops.keys.count": "{total} upstream keys · {on} enabled", "ops.keys.allOn": "All enabled", "ops.keys.someOff": "{n} disabled", "ops.keys.allOff": "All disabled",