feat(monitor): 30-day raw retention + timeline 4-tier style + CC template seed + JSON format button

- History retention 1d → 30d(60s × 30d ≈ 43200 行/model,PG 无压力);
  ComputeAvailability* 不再 UNION rollup 表,直接扫 histories 精度更高。
- Timeline bar 四级高度+颜色双重编码:operational 高+绿 / degraded 中+黄 /
  failed+error 短+红 / 未测试 很短+灰。
- migration 113 seed「Claude Code 伪装」模板(ON CONFLICT DO NOTHING)。
  user_id 用 legacy 格式(user_<64hex>_account_<uuid>_session_<uuid>),
  避免新版 JSON 字符串内嵌 JSON 在编辑器里一长串 \" 难读。
- MonitorAdvancedRequestConfig 加「格式化」按钮 + white-space:pre
  让 body textarea 对长字符串不压扁。
This commit is contained in:
erio
2026-04-21 15:24:48 +08:00
parent 6925ac25c4
commit a7415d4d2e
7 changed files with 102 additions and 64 deletions
@@ -38,12 +38,24 @@
<!-- Body JSON (仅当 mode != off) -->
<div v-if="bodyOverrideMode !== 'off'">
<label class="input-label">{{ t('admin.channelMonitor.advanced.bodyJson') }}</label>
<div class="mb-1 flex items-center justify-between">
<label class="input-label !mb-0">{{ t('admin.channelMonitor.advanced.bodyJson') }}</label>
<button
type="button"
class="text-xs text-primary-600 hover:underline disabled:cursor-not-allowed disabled:text-gray-400 disabled:no-underline dark:text-primary-400"
:disabled="!bodyText.trim()"
@click="formatBody"
>
{{ t('admin.channelMonitor.advanced.bodyJsonFormat') }}
</button>
</div>
<textarea
v-model="bodyText"
rows="8"
rows="10"
:placeholder="bodyPlaceholder"
class="input font-mono text-xs"
style="white-space: pre; overflow-wrap: normal; overflow-x: auto;"
spellcheck="false"
@blur="commitBody"
/>
<p v-if="bodyError" class="mt-1 text-xs text-red-500">{{ bodyError }}</p>
@@ -158,6 +170,25 @@ function commitBody() {
}
}
function formatBody() {
const trimmed = bodyText.value.trim()
if (trimmed === '') return
try {
const parsed = JSON.parse(trimmed)
bodyText.value = JSON.stringify(parsed, null, 2)
bodyError.value = ''
// 同步把校验过的对象提交,避免格式化后焦点未移走时父组件读到旧值
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
emit('update:bodyOverride', parsed as Record<string, unknown>)
}
} catch (e) {
bodyError.value =
t('admin.channelMonitor.advanced.bodyJsonError') +
': ' +
(e instanceof Error ? e.message : String(e))
}
}
function serializeBody(body: Record<string, unknown> | null): string {
if (!body || Object.keys(body).length === 0) return ''
return JSON.stringify(body, null, 2)