@hedeqiang/dsh-nl2sql 0.14.3 → 0.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.14.5] - 2026-11-13
9
+
10
+ ### Changed
11
+ - **Data health settings** are laid out like the Settings page: two groups (Data freshness, Data quality patrol) with switches and one explanation per row; a group's fields are hidden while it is off. With freshness off, its table and events are no longer shown on the Data health page either — just a note and the switch.
12
+
13
+ ## [0.14.4] - 2026-11-13
14
+
15
+ ### Added
16
+ - Data freshness can be switched off, or hidden from the page only (**Manage → Data health → Settings**). When off, nothing is shown under results or in Schema, no scheduled checks run and the agent's `nl2sql_freshness` tool reports that it is off; "hide on page" keeps checks and notifications but removes the warning bars and the Schema panel. The Data health page keeps showing the data either way.
17
+
8
18
  ## [0.14.3] - 2026-11-13
9
19
 
10
20
  ### Changed
@@ -159,7 +169,9 @@ These features are tested with local databases and fake IM / Lark servers; real
159
169
 
160
170
  - Redesigned UI; Lark push (Markdown and native charts); access control, feedback loop, dashboard global filters; PostgreSQL / Elasticsearch, SSH tunnels, privacy settings, automatic overviews, scheduled reports; semantic layer, ER graph, scan guard, history and audit, dashboards; first version.
161
171
 
162
- [Unreleased]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.3...HEAD
172
+ [Unreleased]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.5...HEAD
173
+ [0.14.5]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.4...v0.14.5
174
+ [0.14.4]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.3...v0.14.4
163
175
  [0.14.3]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.2...v0.14.3
164
176
  [0.14.2]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.1...v0.14.2
165
177
  [0.14.1]: https://github.com/hedeqiang/dsh-nl2sql/compare/v0.14.0...v0.14.1
package/lib/client.js CHANGED
@@ -17,6 +17,20 @@ window.__ModuleLoader__.load({
17
17
  // ------------------------------------------------------------ locale
18
18
 
19
19
  const zh = {
20
+ 'fq.admin.fOffNote': "数据新鲜度已关闭:不检查、不推送,页面上也不显示。需要时在下方「设置」中开启。",
21
+ 'fq.set.gFresh': "数据新鲜度",
22
+ 'fq.set.gQuality': "数据质量巡检",
23
+ 'fq.set.fEnabledShort': "启用",
24
+ 'fq.set.fEnabledHint': "定时检查各表最新数据时间,滞后时推送通知;Agent 回答「今天 / 昨天」等近期数据前会先检查。关闭后以上全部停止,页面上也不再显示。",
25
+ 'fq.set.showShort': "在页面上显示",
26
+ 'fq.set.showHint': "查询结果下方的「数据滞后」提示和表结构页的新鲜度面板。隐藏后仍会定时检查和推送通知。",
27
+ 'fq.set.staleHint': "超过这个时长没有新数据即视为滞后;每张表可在上方表格单独设置,0 表示不监控。",
28
+ 'fq.set.fChannelHint': "每次滞后只推送一次,恢复时再推送一次。",
29
+ 'fq.set.qEnabledHint': "每天定时检查空值、重复键、异常值、未来时间与负值,并给出健康分。",
30
+ 'fq.set.on': "已开启",
31
+ 'fq.set.off': "已关闭",
32
+ 'fq.set.shown': "显示",
33
+ 'fq.set.hidden': "已隐藏",
20
34
  'mcp.client.claudeCode': 'Claude Code', 'mcp.client.cline': 'Cline', 'mcp.client.windsurf': 'Windsurf', 'mcp.client.codex': 'Codex CLI', 'mcp.client.gemini': 'Gemini CLI', 'mcp.client.zed': 'Zed', 'mcp.client.continue': 'Continue',
21
35
  'mcp.where.claudeCode': '在终端运行(HTTP 直连;下方为 stdio 桥接写法)。', 'mcp.where.cline': '写入 cline_mcp_settings.json(Cline 面板 → MCP Servers → Configure)。', 'mcp.where.windsurf': '写入 Windsurf 的 mcp_config.json(Cascade → MCP → 打开配置文件)。', 'mcp.where.codex': '写入 ~/.codex/config.toml(或项目的 .codex/config.toml)。', 'mcp.where.gemini': '写入 ~/.gemini/settings.json 或项目的 .gemini/settings.json。', 'mcp.where.zed': '写入 Zed 的 settings.json。', 'mcp.where.continue': '保存为工作区 .continue/mcpServers/nl2sql.yaml。',
22
36
  'mcp.test': '测试连接', 'mcp.testOk': '连接正常:发现 {n} 个工具。', 'mcp.testFail': '连接失败:{error}', 'mcp.testFailSteps': '连接测试未通过:',
@@ -80,7 +94,6 @@ window.__ModuleLoader__.load({
80
94
  "fq.admin.qOff": "每日质量巡检已关闭",
81
95
  "fq.admin.noDs": "请先添加数据源",
82
96
  "fq.set.title": "设置",
83
- "fq.set.fEnabled": "定时检查数据新鲜度",
84
97
  "fq.set.qEnabled": "每日数据质量巡检",
85
98
  "fq.set.interval": "新鲜度检查间隔(分钟)",
86
99
  "fq.set.time": "质量巡检时间",
@@ -89,7 +102,6 @@ window.__ModuleLoader__.load({
89
102
  "fq.set.fChannel": "数据延迟通知渠道",
90
103
  "fq.set.qChannel": "健康分下降通知渠道",
91
104
  "fq.set.noPush": "不推送",
92
- "fq.set.hint": "每张表的阈值可在上方表格中单独设置(0 表示不监控);延迟通知每个滞后周期只推送一次,恢复时再推送一次。",
93
105
  "fq.saved": "已保存",
94
106
  'pro.close': '关闭', 'pro.sendNow': '立即推送', 'pro.sent': '已推送', 'pro.deleteConfirm': '删除「{name}」?', 'pro.weekday': '星期',
95
107
  'pro.wd.0': '周日', 'pro.wd.1': '周一', 'pro.wd.2': '周二', 'pro.wd.3': '周三', 'pro.wd.4': '周四', 'pro.wd.5': '周五', 'pro.wd.6': '周六',
@@ -1178,6 +1190,20 @@ window.__ModuleLoader__.load({
1178
1190
  'policy.tablesLabel': '可访问的表',
1179
1191
  }
1180
1192
  const en = {
1193
+ 'fq.admin.fOffNote': "Data freshness is off: no checks, no notifications, nothing shown on the page. Turn it on in Settings below.",
1194
+ 'fq.set.gFresh': "Data freshness",
1195
+ 'fq.set.gQuality': "Data quality patrol",
1196
+ 'fq.set.fEnabledShort': "Enabled",
1197
+ 'fq.set.fEnabledHint': "Checks the latest timestamp of every table on a schedule and notifies when data stops updating; the agent checks it before answering about recent periods. When off, all of this stops and nothing is shown on the page.",
1198
+ 'fq.set.showShort': "Show on the page",
1199
+ 'fq.set.showHint': "Stale-data bars under results and the freshness panel in Schema. Checks and notifications keep running when hidden.",
1200
+ 'fq.set.staleHint': "Data older than this counts as stale; set per-table thresholds in the table above (0 = not monitored).",
1201
+ 'fq.set.fChannelHint': "One notification per stale episode, and one when it recovers.",
1202
+ 'fq.set.qEnabledHint': "Daily check of null rates, duplicate keys, outliers, future timestamps and negative amounts, with a health score.",
1203
+ 'fq.set.on': "On",
1204
+ 'fq.set.off': "Off",
1205
+ 'fq.set.shown': "Shown",
1206
+ 'fq.set.hidden': "Hidden",
1181
1207
  'mcp.client.claudeCode': 'Claude Code', 'mcp.client.cline': 'Cline', 'mcp.client.windsurf': 'Windsurf', 'mcp.client.codex': 'Codex CLI', 'mcp.client.gemini': 'Gemini CLI', 'mcp.client.zed': 'Zed', 'mcp.client.continue': 'Continue',
1182
1208
  'mcp.where.claudeCode': 'Run in a terminal (HTTP; the stdio bridge variant is below).', 'mcp.where.cline': 'Add to cline_mcp_settings.json (Cline panel → MCP Servers → Configure).', 'mcp.where.windsurf': 'Add to Windsurf\'s mcp_config.json (Cascade → MCP → open config file).', 'mcp.where.codex': 'Add to ~/.codex/config.toml (or the project\'s .codex/config.toml).', 'mcp.where.gemini': 'Add to ~/.gemini/settings.json or the project\'s .gemini/settings.json.', 'mcp.where.zed': 'Add to Zed\'s settings.json.', 'mcp.where.continue': 'Save as .continue/mcpServers/nl2sql.yaml in the workspace.',
1183
1209
  'mcp.test': 'Test connection', 'mcp.testOk': 'Connected: {n} tools discovered.', 'mcp.testFail': 'Connection failed: {error}', 'mcp.testFailSteps': 'Connection test failed:',
@@ -1241,7 +1267,6 @@ window.__ModuleLoader__.load({
1241
1267
  "fq.admin.qOff": "Daily quality patrol is off",
1242
1268
  "fq.admin.noDs": "Add a data source first",
1243
1269
  "fq.set.title": "Settings",
1244
- "fq.set.fEnabled": "Check data freshness on a schedule",
1245
1270
  "fq.set.qEnabled": "Daily data quality patrol",
1246
1271
  "fq.set.interval": "Freshness check interval (min)",
1247
1272
  "fq.set.time": "Quality patrol time",
@@ -1250,7 +1275,6 @@ window.__ModuleLoader__.load({
1250
1275
  "fq.set.fChannel": "Delay notification channel",
1251
1276
  "fq.set.qChannel": "Score drop notification channel",
1252
1277
  "fq.set.noPush": "Do not push",
1253
- "fq.set.hint": "Per-table thresholds can be set in the table above (0 = not monitored). A delay is pushed once per stale episode, and once more when it recovers.",
1254
1278
  "fq.saved": "Saved",
1255
1279
  'pro.close': 'Close', 'pro.sendNow': 'Send now', 'pro.sent': 'Sent', 'pro.deleteConfirm': 'Delete "{name}"?', 'pro.weekday': 'Weekday',
1256
1280
  'pro.wd.0': 'Sunday', 'pro.wd.1': 'Monday', 'pro.wd.2': 'Tuesday', 'pro.wd.3': 'Wednesday', 'pro.wd.4': 'Thursday', 'pro.wd.5': 'Friday', 'pro.wd.6': 'Saturday',
@@ -2367,6 +2391,9 @@ window.__ModuleLoader__.load({
2367
2391
  .n2s-mcp-steps{margin:6px 0 0;padding-left:18px;font-size:12.5px}
2368
2392
  .n2s-mcp-log .n2s-select{max-width:200px}
2369
2393
  .n2s-mcp-logtable td{vertical-align:middle}
2394
+ .n2s-fq-settings{padding-top:6px}
2395
+ .n2s-fq-setgroup{margin:14px 0 2px;font-size:12.5px;font-weight:600;color:var(--n2s-muted);letter-spacing:.02em}
2396
+ .n2s-fq-setgroup:first-child{margin-top:4px}
2370
2397
  /* ---- dashboard v2 ---- */
2371
2398
  .n2s-srcbadge{display:inline-flex;align-items:center;gap:3px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:none}
2372
2399
  .n2s-seg.small button{height:26px;padding:0 10px;font-size:12px}
@@ -6630,19 +6657,21 @@ window.__ModuleLoader__.load({
6630
6657
  const key = `${datasourceId}\u0000${sql ?? ''}`
6631
6658
  const hit = map.get(key)
6632
6659
  if (hit && Date.now() - hit.at < TTL) return hit.promise
6633
- const promise = call('freshnessFor', { datasourceId, ...(sql ? { sql } : {}) })
6660
+ const promise = call('freshnessFor', { datasourceId, panel: true, ...(sql ? { sql } : {}) })
6634
6661
  map.set(key, { at: Date.now(), promise })
6635
6662
  promise.catch(() => map.delete(key))
6636
6663
  return promise
6637
6664
  },
6638
6665
  async refresh(datasourceId) {
6639
- const v = await call('freshnessFor', { datasourceId, refresh: true })
6666
+ const v = await call('freshnessFor', { datasourceId, refresh: true, panel: true })
6640
6667
  for (const k of [...map.keys()]) if (k.startsWith(`${datasourceId}\u0000`)) map.delete(k)
6641
6668
  map.set(`${datasourceId}\u0000`, { at: Date.now(), promise: Promise.resolve(v) })
6642
6669
  rev++
6643
6670
  for (const f of subs) f()
6644
6671
  return v
6645
6672
  },
6673
+ /** Drop everything (e.g. after the show / enable settings change) and re-render subscribers. */
6674
+ reset() { map.clear(); rev++; for (const f of subs) f() },
6646
6675
  subscribe(f) { subs.add(f); return () => subs.delete(f) },
6647
6676
  rev: () => rev,
6648
6677
  }
@@ -6691,6 +6720,7 @@ window.__ModuleLoader__.load({
6691
6720
  fqCache.get(dsId, sql).then(v => alive && setView(v), () => {})
6692
6721
  return () => { alive = false }
6693
6722
  }, [dsId, sql, rev])
6723
+ if (view?.disabled) return null
6694
6724
  const stale = (view?.tables ?? []).filter(x => x.stale)
6695
6725
  if (!stale.length) return null
6696
6726
  return h('div', { className: 'n2s-fq-bar', role: 'status' },
@@ -6719,7 +6749,7 @@ window.__ModuleLoader__.load({
6719
6749
  return () => { alive = false }
6720
6750
  }, [ds?.id, rev])
6721
6751
  const v = state.value
6722
- if (!ds || state.error || !v || !v.tables?.length) return null
6752
+ if (!ds || state.error || !v || v.disabled || !v.tables?.length) return null
6723
6753
  const stale = v.tables.filter(x => x.stale).length
6724
6754
  const warn = v.tables.filter(x => x.status === 'warn').length
6725
6755
  const expanded = open ?? stale > 0
@@ -6786,28 +6816,35 @@ window.__ModuleLoader__.load({
6786
6816
  const { overrides: _o, ...freshness } = form.freshness
6787
6817
  const out = await call('saveHealthSettings', { freshness: { ...freshness, intervalMin: Number(freshness.intervalMin), staleHours: Number(freshness.staleHours), channelId: freshness.channelId || null }, quality: { ...form.quality, dropPoints: Number(form.quality.dropPoints), channelId: form.quality.channelId || null } })
6788
6818
  setForm(structuredClone(out))
6819
+ fqCache.reset()
6789
6820
  notify(t('fq.saved'), 'ok')
6790
6821
  onSaved?.()
6791
6822
  } catch (e) { notifyError(e) }
6792
6823
  })
6793
- return h('div', { className: 'n2s-card' },
6794
- h('div', { className: 'n2s-form' },
6795
- h('label', { className: 'n2s-check' }, h('input', { type: 'checkbox', checked: Boolean(form.freshness.enabled), onChange: e => setF('enabled', e.target.checked) }), t('fq.set.fEnabled')),
6796
- h('label', { className: 'n2s-check' }, h('input', { type: 'checkbox', checked: Boolean(form.quality.enabled), onChange: e => setQ('enabled', e.target.checked) }), t('fq.set.qEnabled')),
6797
- h('label', { className: 'n2s-field' }, h('span', null, t('fq.set.interval')), h('input', { className: 'n2s-input', type: 'number', min: 5, max: 1440, value: form.freshness.intervalMin, onChange: e => setF('intervalMin', e.target.value) })),
6798
- h('label', { className: 'n2s-field' }, h('span', null, t('fq.set.time')), h('input', { className: 'n2s-input', type: 'time', value: form.quality.time, onChange: e => setQ('time', e.target.value) })),
6799
- h('label', { className: 'n2s-field' }, h('span', null, t('fq.set.staleHours')), h('input', { className: 'n2s-input', type: 'number', min: 0.25, step: 0.25, value: form.freshness.staleHours, onChange: e => setF('staleHours', e.target.value) })),
6800
- h('label', { className: 'n2s-field' }, h('span', null, t('fq.set.drop')), h('input', { className: 'n2s-input', type: 'number', min: 1, max: 100, value: form.quality.dropPoints, onChange: e => setQ('dropPoints', e.target.value) })),
6801
- h('div', { className: 'n2s-field' }, h('label', { htmlFor: 'n2s-fq-fch', className: 'n2s-flabel' }, t('fq.set.fChannel')), h(ChannelPicker, { t, id: 'n2s-fq-fch', value: form.freshness.channelId ?? '', noneLabel: t('fq.set.noPush'), onChange: v => setF('channelId', v) })),
6802
- h('div', { className: 'n2s-field' }, h('label', { htmlFor: 'n2s-fq-qch', className: 'n2s-flabel' }, t('fq.set.qChannel')), h(ChannelPicker, { t, id: 'n2s-fq-qch', value: form.quality.channelId ?? '', noneLabel: t('fq.set.noPush'), onChange: v => setQ('channelId', v) }))),
6803
- h('div', { className: 'n2s-faint n2s-small', style: { marginTop: 10 } }, t('fq.set.hint')),
6804
- h('div', { className: 'n2s-row', style: { marginTop: 12, justifyContent: 'flex-end' } },
6824
+ const fOn = Boolean(form.freshness.enabled)
6825
+ const qOn = Boolean(form.quality.enabled)
6826
+ const sw = (checked, onChange, label, disabled) => h('label', { className: 'n2s-switch' }, h('input', { type: 'checkbox', role: 'switch', checked, disabled, 'aria-label': label, onChange: e => onChange(e.target.checked) }), h('span', null, label))
6827
+ const num = (id, value, onChange, attrs) => h('input', { id, className: 'n2s-input', type: 'number', style: { maxWidth: 160 }, value, onChange: e => onChange(e.target.value), ...attrs })
6828
+ const picker = (id, value, onChange) => h('div', { style: { maxWidth: 360 } }, h(ChannelPicker, { t, id, value: value ?? '', noneLabel: t('fq.set.noPush'), onChange }))
6829
+ return h('div', { className: 'n2s-card n2s-fq-settings' },
6830
+ h('div', { className: 'n2s-fq-setgroup' }, t('fq.set.gFresh')),
6831
+ h(SettingRow, { label: t('fq.set.fEnabledShort'), hint: t('fq.set.fEnabledHint') }, sw(fOn, v => setF('enabled', v), fOn ? t('fq.set.on') : t('fq.set.off'))),
6832
+ fOn && h(SettingRow, { label: t('fq.set.showShort'), hint: t('fq.set.showHint') }, sw(form.freshness.showInUi !== false, v => setF('showInUi', v), form.freshness.showInUi !== false ? t('fq.set.shown') : t('fq.set.hidden'))),
6833
+ fOn && h(SettingRow, { label: t('fq.set.interval'), htmlFor: 'n2s-fq-int' }, num('n2s-fq-int', form.freshness.intervalMin, v => setF('intervalMin', v), { min: 5, max: 1440 })),
6834
+ fOn && h(SettingRow, { label: t('fq.set.staleHours'), htmlFor: 'n2s-fq-stale', hint: t('fq.set.staleHint') }, num('n2s-fq-stale', form.freshness.staleHours, v => setF('staleHours', v), { min: 0.25, step: 0.25 })),
6835
+ fOn && h(SettingRow, { label: t('fq.set.fChannel'), htmlFor: 'n2s-fq-fch', hint: t('fq.set.fChannelHint') }, picker('n2s-fq-fch', form.freshness.channelId, v => setF('channelId', v))),
6836
+ h('div', { className: 'n2s-fq-setgroup' }, t('fq.set.gQuality')),
6837
+ h(SettingRow, { label: t('fq.set.qEnabled'), hint: t('fq.set.qEnabledHint') }, sw(qOn, v => setQ('enabled', v), qOn ? t('fq.set.on') : t('fq.set.off'))),
6838
+ qOn && h(SettingRow, { label: t('fq.set.time'), htmlFor: 'n2s-fq-time' }, h('input', { id: 'n2s-fq-time', className: 'n2s-input', type: 'time', style: { maxWidth: 160 }, value: form.quality.time, onChange: e => setQ('time', e.target.value) })),
6839
+ qOn && h(SettingRow, { label: t('fq.set.qChannel'), htmlFor: 'n2s-fq-qch' }, picker('n2s-fq-qch', form.quality.channelId, v => setQ('channelId', v))),
6840
+ qOn && form.quality.channelId && h(SettingRow, { label: t('fq.set.drop'), htmlFor: 'n2s-fq-drop' }, num('n2s-fq-drop', form.quality.dropPoints, v => setQ('dropPoints', v), { min: 1, max: 100 })),
6841
+ h('div', { className: 'n2s-row', style: { marginTop: 14, justifyContent: 'flex-end' } },
6805
6842
  h('button', { type: 'button', className: 'n2s-btn primary', disabled: busy, onClick: save }, busy ? h('span', { className: 'n2s-spin' }) : null, t('ds.save'))))
6806
6843
  }
6807
6844
 
6808
6845
  function HealthFreshnessTable({ t, dsId }) {
6809
6846
  const rev = useFqRev()
6810
- const fr = useAsync(() => (dsId ? fqCache.get(dsId) : null), [dsId, rev])
6847
+ const fr = useAsync(() => (dsId ? call('freshnessFor', { datasourceId: dsId }) : null), [dsId, rev])
6811
6848
  const settings = useAsync(() => call('healthSettings'), [dsId, rev])
6812
6849
  const events = useAsync(() => (dsId ? call('freshnessEvents', { datasourceId: dsId, limit: 10 }) : []), [dsId, rev])
6813
6850
  const [busy, setBusy] = useState(false)
@@ -6898,7 +6935,10 @@ window.__ModuleLoader__.load({
6898
6935
  ' · ',
6899
6936
  status.value.quality.enabled ? fmt(t, 'fq.admin.nextQ', { time: status.value.quality.time }) : t('fq.admin.qOff')),
6900
6937
  !dsId ? h('div', { className: 'n2s-empty' }, h('h3', null, t('fq.admin.noDs'))) : h(React.Fragment, null,
6901
- h(HealthFreshnessTable, { key: `f-${dsId}`, t, dsId }),
6938
+ // When freshness is turned off, its table and events are not shown (and not queried).
6939
+ status.value && !status.value.freshness.enabled
6940
+ ? h('div', { className: 'n2s-notice', style: { marginBottom: 6 } }, t('fq.admin.fOffNote'))
6941
+ : h(HealthFreshnessTable, { key: `f-${dsId}`, t, dsId }),
6902
6942
  h(HealthQuality, { key: `q-${dsId}`, t, dsId })),
6903
6943
  h('div', { className: 'n2s-setsub' }, t('fq.set.title')),
6904
6944
  h(HealthSettings, { t, onSaved: () => status.reload() }))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hedeqiang/dsh-nl2sql",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "description": "Ask MySQL, PostgreSQL, Elasticsearch and CSV/Excel in plain language inside DeepSeek Harness: charts, dashboards, alerts, and IM push and bots for Lark/Feishu, Telegram, WeCom and DingTalk.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,8 @@ import { UserError, lastDailySlot, newId } from './shared.js'
19
19
 
20
20
  // ------------------------------------------------------------------ constants
21
21
 
22
- export const FRESHNESS_DEFAULTS = { enabled: true, intervalMin: 30, staleHours: 24, overrides: {}, channelId: null }
22
+ // enabled: scheduled checks + notifications + the agent tool; showInUi: warning bars under results and the schema panel.
23
+ export const FRESHNESS_DEFAULTS = { enabled: true, showInUi: true, intervalMin: 30, staleHours: 24, overrides: {}, channelId: null }
23
24
  export const QUALITY_DEFAULTS = { enabled: false, time: '06:30', channelId: null, dropPoints: 10, maxTableRows: 5000000, maxTables: 60 }
24
25
  const DATE_TYPES = new Set(['date', 'datetime', 'timestamp', 'timestamptz'])
25
26
  const NUM_TYPES = new Set(['tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint', 'decimal', 'numeric', 'float', 'double', 'real'])
@@ -271,6 +272,7 @@ export const methods = {
271
272
  if (freshness) {
272
273
  const s = this.freshnessState().settings
273
274
  if (freshness.enabled !== undefined) s.enabled = Boolean(freshness.enabled)
275
+ if (freshness.showInUi !== undefined) s.showInUi = Boolean(freshness.showInUi)
274
276
  if (freshness.intervalMin !== undefined) s.intervalMin = Math.round(intIn(freshness.intervalMin, 5, 1440, 'intervalMin'))
275
277
  if (freshness.staleHours !== undefined) s.staleHours = intIn(freshness.staleHours, 0.25, 24 * 90, 'staleHours')
276
278
  if (freshness.channelId !== undefined) s.channelId = channel(freshness.channelId)
@@ -422,9 +424,12 @@ export const methods = {
422
424
  },
423
425
 
424
426
  /** RPC: stored freshness (computed on first use or when refresh); `sql` narrows to referenced tables. */
425
- async freshnessFor({ datasourceId, refresh = false, sql } = {}) {
427
+ async freshnessFor({ datasourceId, refresh = false, sql, panel = false } = {}) {
426
428
  const ds = this.requireDatasource(datasourceId ?? this.state.current)
427
429
  const f = this.freshnessState()
430
+ // Page panels (result bars, schema panel) ask with panel: true; when the feature or its
431
+ // page display is off they get nothing and no query runs.
432
+ if (panel && (!f.settings.enabled || f.settings.showInUi === false)) return { datasourceId: ds.id, disabled: true, tables: [] }
428
433
  let store = f[ds.id]
429
434
  if (refresh || !store) {
430
435
  if (refresh) store = await this.withJobLock(`freshness:${ds.id}`, () => this.refreshFreshness(ds))
@@ -737,6 +742,7 @@ export const FEATURE = {
737
742
  presentCall: (args) => ({ card: 'generic', title: args?.tables?.length ? `数据新鲜度 ${args.tables.join(', ')}` : '数据新鲜度', kind: 'read' }),
738
743
  execute: async (args, exec) => {
739
744
  try {
745
+ if (!service.freshnessState().settings.enabled) return { disabled: true, message: 'Data freshness checks are turned off by the administrator (Data Q&A → Manage → Data health). Answer without freshness warnings.' }
740
746
  return await service.freshnessForSession({ sessionId: exec?.agent?.id, datasource: args?.datasource, tables: args?.tables })
741
747
  } catch (error) {
742
748
  throw error instanceof UserError ? new Error(error.message) : error