<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import {
  CircleCheckFilled,
  Refresh,
  WarningFilled,
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import http, { apiErrorMessage } from '../api/http'
import AppUpdateManagementPanel from '../components/AppUpdateManagementPanel.vue'
import PerformanceReportPanel from '../components/PerformanceReportPanel.vue'

type HealthStatus = 'healthy' | 'warning' | 'unhealthy'

interface HealthComponent {
  key: string
  label: string
  status: HealthStatus
  backend: string
  latencyMilliseconds?: number
  detail: string
}

interface OperationalAlert {
  id: string
  severity: 'critical' | 'warning'
  source: string
  title: string
  detail: string
  occurredAt: string
}

interface BackupArtifact {
  id: string
  fileName: string
  provider: string
  createdAt: string
  sizeBytes: number
  sha256: string
  note?: string
  lastDrillAt?: string
  lastDrillSucceeded?: boolean
  lastDrillDetail?: string
  lastDrillDurationMilliseconds?: number
}

interface Summary {
  generatedAt: string
  health: {
    checkedAt: string
    overallStatus: HealthStatus
    components: HealthComponent[]
    backlog?: {
      pending: number
      publishing: number
      published: number
      processing: number
      expiredLeases: number
      oldestUnfinishedAgeSeconds?: number
    }
  }
  counters: {
    auditEvents24Hours: number
    serverErrors24Hours: number
    failedJobs7Days: number
    criticalAlerts: number
  }
  alerts: OperationalAlert[]
  latestBackup?: BackupArtifact
}

interface AuditLog {
  id: string
  userName?: string
  method: string
  path: string
  statusCode: number
  ipAddress?: string
  createdAt: string
}

interface FailedJob {
  id: string
  kind: string
  kindLabel: string
  context: string
  errorMessage: string
  createdAt: string
  startedAt?: string
  completedAt?: string
  processingAttempts: number
}

interface PageResult<T> {
  items: T[]
  total: number
  page: number
  pageSize: number
}

interface SwaggerDocumentationSettings {
  isEnabled: boolean
}

const loading = ref(true)
const refreshing = ref(false)
const backupBusy = ref(false)
const summary = ref<Summary>()
const backups = ref<BackupArtifact[]>([])
const activeLedger = ref<'audit' | 'jobs'>('audit')
const swaggerEnabled = ref(false)
const swaggerBusy = ref(false)

const auditLoading = ref(false)
const auditRows = ref<AuditLog[]>([])
const auditTotal = ref(0)
const auditFilter = reactive({
  page: 1,
  pageSize: 20,
  method: '',
  statusCode: '',
  userName: '',
  path: '',
  range: [] as Date[],
})

const jobsLoading = ref(false)
const jobRows = ref<FailedJob[]>([])
const jobTotal = ref(0)
const jobFilter = reactive({
  page: 1,
  pageSize: 20,
  kind: '',
  range: [] as Date[],
})

const healthTrack = computed(() => {
  const components = summary.value?.health.components ?? []
  const backup = summary.value?.latestBackup
  return [
    ...components,
    {
      key: 'backup',
      label: '可恢复备份',
      backend: backup?.provider?.toLowerCase() ?? 'none',
      status: !backup
        ? 'unhealthy'
        : backup.lastDrillSucceeded === true
          ? 'healthy'
          : backup.lastDrillSucceeded === false
            ? 'unhealthy'
            : 'warning',
      detail: !backup
        ? '尚未创建备份。'
        : backup.lastDrillSucceeded === true
          ? '最近备份已通过隔离恢复演练。'
          : backup.lastDrillSucceeded === false
            ? backup.lastDrillDetail ?? '恢复演练失败。'
            : '备份存在，但尚未执行恢复演练。',
    } as HealthComponent,
  ]
})

const overallStatus = computed<HealthStatus>(() => {
  if (!summary.value) return 'warning'
  if (
    summary.value.health.overallStatus === 'unhealthy' ||
    summary.value.alerts.some((alert) => alert.severity === 'critical')
  ) return 'unhealthy'
  if (
    summary.value.health.overallStatus === 'warning' ||
    summary.value.alerts.length > 0
  ) return 'warning'
  return 'healthy'
})

function formatTime(value?: string) {
  if (!value) return '—'
  return new Date(value).toLocaleString('zh-CN', {
    hour12: false,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
  })
}

function formatBytes(value: number) {
  if (value < 1024) return `${value} B`
  if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} KB`
  if (value < 1024 ** 3) return `${(value / 1024 ** 2).toFixed(1)} MB`
  return `${(value / 1024 ** 3).toFixed(2)} GB`
}

function healthLabel(status: HealthStatus) {
  return status === 'healthy' ? '正常' : status === 'warning' ? '注意' : '异常'
}

function statusClass(status: HealthStatus) {
  return `is-${status}`
}

function httpStatusType(status: number) {
  if (status >= 500) return 'danger'
  if (status >= 400) return 'warning'
  if (status >= 300) return 'info'
  return 'success'
}

async function loadSummary() {
  const [{ data: summaryData }, { data: backupData }, { data: swaggerData }] = await Promise.all([
    http.get<Summary>('/operations/summary'),
    http.get<BackupArtifact[]>('/operations/backups'),
    http.get<SwaggerDocumentationSettings>('/operations/swagger'),
  ])
  summary.value = summaryData
  backups.value = backupData
  swaggerEnabled.value = swaggerData.isEnabled
}

async function updateSwaggerDocumentation(value: string | number | boolean) {
  if (typeof value !== 'boolean') return
  const isEnabled = value
  swaggerBusy.value = true
  try {
    const { data } = await http.put<SwaggerDocumentationSettings>(
      '/operations/swagger',
      { isEnabled },
    )
    swaggerEnabled.value = data.isEnabled
    ElMessage.success(data.isEnabled ? 'Swagger 文档已开放。' : 'Swagger 文档已关闭。')
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    swaggerBusy.value = false
  }
}

function openSwaggerDocumentation() {
  window.open('/swagger', '_blank', 'noopener')
}

async function loadAudit() {
  auditLoading.value = true
  try {
    const params: Record<string, string | number> = {
      page: auditFilter.page,
      pageSize: auditFilter.pageSize,
    }
    if (auditFilter.method) params.method = auditFilter.method
    if (auditFilter.statusCode) params.statusCode = Number(auditFilter.statusCode)
    if (auditFilter.userName.trim()) params.userName = auditFilter.userName.trim()
    if (auditFilter.path.trim()) params.path = auditFilter.path.trim()
    if (auditFilter.range.length === 2) {
      params.from = auditFilter.range[0].toISOString()
      params.to = auditFilter.range[1].toISOString()
    }
    const { data } = await http.get<PageResult<AuditLog>>(
      '/operations/audit-logs',
      { params },
    )
    auditRows.value = data.items
    auditTotal.value = data.total
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    auditLoading.value = false
  }
}

async function loadJobs() {
  jobsLoading.value = true
  try {
    const params: Record<string, string | number> = {
      page: jobFilter.page,
      pageSize: jobFilter.pageSize,
    }
    if (jobFilter.kind) params.kind = jobFilter.kind
    if (jobFilter.range.length === 2) {
      params.from = jobFilter.range[0].toISOString()
      params.to = jobFilter.range[1].toISOString()
    }
    const { data } = await http.get<PageResult<FailedJob>>(
      '/operations/failed-jobs',
      { params },
    )
    jobRows.value = data.items
    jobTotal.value = data.total
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    jobsLoading.value = false
  }
}

async function refreshAll() {
  refreshing.value = true
  try {
    await Promise.all([loadSummary(), loadAudit(), loadJobs()])
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    refreshing.value = false
    loading.value = false
  }
}

function searchAudit() {
  auditFilter.page = 1
  loadAudit()
}

function resetAudit() {
  Object.assign(auditFilter, {
    page: 1,
    method: '',
    statusCode: '',
    userName: '',
    path: '',
    range: [],
  })
  loadAudit()
}

function searchJobs() {
  jobFilter.page = 1
  loadJobs()
}

async function createBackup() {
  try {
    const { value } = await ElMessageBox.prompt(
      '可填写本次备份的变更窗口、发布版本或值班说明。',
      '创建数据库备份',
      {
        confirmButtonText: '开始备份',
        cancelButtonText: '取消',
        inputPlaceholder: '选填，最多 200 字',
        inputValidator: (text) => text.length <= 200 || '说明不能超过 200 字。',
      },
    )
    backupBusy.value = true
    await http.post(
      '/operations/backups',
      { note: value?.trim() || null },
      { timeout: 30 * 60 * 1000 },
    )
    ElMessage.success('数据库备份已创建。')
    await loadSummary()
  } catch (error) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  } finally {
    backupBusy.value = false
  }
}

async function runDrill(backup: BackupArtifact) {
  try {
    await ElMessageBox.confirm(
      `将把“${backup.fileName}”恢复到隔离数据库并执行完整性检查。当前业务数据库不会被覆盖。`,
      '运行恢复演练',
      {
        type: 'warning',
        confirmButtonText: '开始隔离演练',
        cancelButtonText: '取消',
      },
    )
    backupBusy.value = true
    const { data } = await http.post(
      `/operations/backups/${backup.id}/restore-drill`,
      { confirmation: 'RESTORE_DRILL' },
      { timeout: 30 * 60 * 1000 },
    )
    if (data.succeeded) ElMessage.success(data.detail)
    else ElMessage.error(data.detail)
    await loadSummary()
  } catch (error) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  } finally {
    backupBusy.value = false
  }
}

onMounted(refreshAll)
</script>

<template>
  <main v-loading="loading" class="operations-console">
    <header class="console-heading">
      <div>
        <span class="console-kicker">SYSTEM WATCH / {{ formatTime(summary?.generatedAt) }}</span>
        <h2>运维与审计控制台</h2>
        <p>检查系统是否可用、任务为何失败，以及备份能否真正恢复。</p>
      </div>
      <el-button :icon="Refresh" :loading="refreshing" @click="refreshAll">
        刷新状态
      </el-button>
    </header>

    <section class="swagger-panel">
      <div>
        <span>API DOCUMENTATION</span>
        <h3>Swagger 文档</h3>
        <p>
          {{ swaggerEnabled
            ? '文档与 OpenAPI 定义当前已对外开放。'
            : '文档当前关闭，访问 /swagger 将返回 404。' }}
        </p>
      </div>
      <div class="swagger-actions">
        <el-switch
          :model-value="swaggerEnabled"
          :loading="swaggerBusy"
          active-text="已开放"
          inactive-text="已关闭"
          @change="updateSwaggerDocumentation"
        />
        <el-button
          v-if="swaggerEnabled"
          type="primary"
          plain
          @click="openSwaggerDocumentation"
        >
          打开文档
        </el-button>
      </div>
    </section>

    <section class="signal-board" aria-label="系统健康链路">
      <div class="signal-summary">
        <span>当前值守结论</span>
        <strong :class="statusClass(overallStatus)">
          {{ healthLabel(overallStatus) }}
        </strong>
        <p>
          {{ summary?.counters.criticalAlerts ?? 0 }} 个严重告警 ·
          {{ summary?.counters.auditEvents24Hours ?? 0 }} 次写操作 / 24h
        </p>
      </div>
      <div class="signal-track">
        <article
          v-for="(component, index) in healthTrack"
          :key="component.key"
          :class="statusClass(component.status)"
          class="signal-node"
        >
          <div class="signal-mark">
            <span>{{ index + 1 }}</span>
          </div>
          <div>
            <small>{{ component.backend }}</small>
            <h3>{{ component.label }}</h3>
            <p>{{ component.detail }}</p>
          </div>
          <b>
            {{ healthLabel(component.status) }}
            <template v-if="component.latencyMilliseconds != null">
              · {{ component.latencyMilliseconds }} ms
            </template>
          </b>
        </article>
      </div>
    </section>

    <PerformanceReportPanel />

    <section class="console-split">
      <article class="alerts-panel">
        <div class="panel-heading">
          <div>
            <span>EXCEPTION QUEUE</span>
            <h3>异常告警</h3>
          </div>
          <em>{{ summary?.alerts.length ?? 0 }}</em>
        </div>
        <div v-if="summary?.alerts.length" class="alert-list">
          <div
            v-for="alert in summary.alerts"
            :key="alert.id"
            :class="`is-${alert.severity}`"
            class="alert-row"
          >
            <el-icon>
              <WarningFilled v-if="alert.severity === 'critical'" />
              <CircleCheckFilled v-else />
            </el-icon>
            <div>
              <strong>{{ alert.title }}</strong>
              <p>{{ alert.detail }}</p>
              <small>{{ alert.source.toUpperCase() }} · {{ formatTime(alert.occurredAt) }}</small>
            </div>
          </div>
        </div>
        <div v-else class="clear-state">
          <el-icon><CircleCheckFilled /></el-icon>
          <div>
            <strong>当前没有异常告警</strong>
            <p>健康探针、失败任务与备份时效均在允许范围内。</p>
          </div>
        </div>
      </article>

      <article class="backup-panel">
        <div class="panel-heading">
          <div>
            <span>RECOVERY READINESS</span>
            <h3>备份与恢复演练</h3>
          </div>
          <el-button
            type="primary"
            :loading="backupBusy"
            @click="createBackup"
          >
            创建备份
          </el-button>
        </div>
        <p class="backup-rule">
          恢复演练只使用隔离数据库；校验失败的备份不会进入业务库。
        </p>
        <div v-if="backups.length" class="backup-list">
          <div v-for="backup in backups.slice(0, 5)" :key="backup.id" class="backup-row">
            <div class="backup-stamp">
              <span>{{ backup.provider }}</span>
              <b>{{ formatBytes(backup.sizeBytes) }}</b>
            </div>
            <div class="backup-copy">
              <strong>{{ backup.fileName }}</strong>
              <p>{{ backup.note || '无备份说明' }}</p>
              <small>
                {{ formatTime(backup.createdAt) }} · SHA-256
                {{ backup.sha256.slice(0, 10) }}…
              </small>
            </div>
            <div class="drill-state">
              <span
                :class="backup.lastDrillSucceeded === true
                  ? 'passed'
                  : backup.lastDrillSucceeded === false
                    ? 'failed'
                    : 'pending'"
              >
                {{
                  backup.lastDrillSucceeded === true
                    ? '演练通过'
                    : backup.lastDrillSucceeded === false
                      ? '演练失败'
                      : '待演练'
                }}
              </span>
              <el-button
                link
                type="primary"
                :disabled="backupBusy"
                @click="runDrill(backup)"
              >
                运行恢复演练
              </el-button>
            </div>
          </div>
        </div>
        <div v-else class="empty-backup">
          尚无备份。创建首个备份后，立即运行一次恢复演练。
        </div>
      </article>
    </section>

    <AppUpdateManagementPanel />

    <section class="ledger-panel">
      <div class="ledger-tabs" role="tablist" aria-label="审计查询类型">
        <button
          type="button"
          :class="{ active: activeLedger === 'audit' }"
          @click="activeLedger = 'audit'"
        >
          操作日志
          <span>{{ auditTotal }}</span>
        </button>
        <button
          type="button"
          :class="{ active: activeLedger === 'jobs' }"
          @click="activeLedger = 'jobs'"
        >
          失败后台任务
          <span>{{ jobTotal }}</span>
        </button>
      </div>

      <div v-if="activeLedger === 'audit'" class="ledger-content">
        <div class="query-strip">
          <el-select v-model="auditFilter.method" clearable placeholder="请求方法">
            <el-option v-for="method in ['POST', 'PUT', 'PATCH', 'DELETE']" :key="method" :value="method" />
          </el-select>
          <el-input v-model="auditFilter.statusCode" clearable placeholder="状态码，如 500" />
          <el-input v-model="auditFilter.userName" clearable placeholder="操作账号" />
          <el-input v-model="auditFilter.path" clearable placeholder="接口路径" />
          <el-date-picker
            v-model="auditFilter.range"
            type="datetimerange"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            range-separator="至"
          />
          <el-button type="primary" @click="searchAudit">查询</el-button>
          <el-button @click="resetAudit">重置</el-button>
        </div>
        <el-table v-loading="auditLoading" :data="auditRows" stripe>
          <el-table-column label="时间" width="172">
            <template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
          </el-table-column>
          <el-table-column prop="userName" label="账号" min-width="120">
            <template #default="{ row }">{{ row.userName || '未认证' }}</template>
          </el-table-column>
          <el-table-column label="操作" width="96">
            <template #default="{ row }">
              <span class="method-pill">{{ row.method }}</span>
            </template>
          </el-table-column>
          <el-table-column prop="path" label="接口路径" min-width="260" show-overflow-tooltip />
          <el-table-column label="结果" width="90">
            <template #default="{ row }">
              <el-tag :type="httpStatusType(row.statusCode)" effect="plain">
                {{ row.statusCode }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="ipAddress" label="来源 IP" width="142">
            <template #default="{ row }">{{ row.ipAddress || '—' }}</template>
          </el-table-column>
          <template #empty>
            <span>该时间范围内没有匹配的写操作。</span>
          </template>
        </el-table>
        <el-pagination
          v-model:current-page="auditFilter.page"
          v-model:page-size="auditFilter.pageSize"
          background
          layout="total, prev, pager, next"
          :total="auditTotal"
          @current-change="loadAudit"
        />
      </div>

      <div v-else class="ledger-content">
        <div class="query-strip jobs-query">
          <el-select v-model="jobFilter.kind" clearable placeholder="任务类型">
            <el-option label="自动排课" value="AutomaticSchedule" />
            <el-option label="课表发布" value="SchedulePublish" />
            <el-option label="补考自动安排" value="MakeupExamAuto" />
            <el-option label="考试与补考编排" value="ExamArrangement" />
          </el-select>
          <el-date-picker
            v-model="jobFilter.range"
            type="datetimerange"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            range-separator="至"
          />
          <el-button type="primary" @click="searchJobs">查询</el-button>
        </div>
        <div v-loading="jobsLoading" class="job-list">
          <article v-for="job in jobRows" :key="job.id" class="job-row">
            <div>
              <span>{{ job.kindLabel }}</span>
              <strong>{{ job.context }}</strong>
              <small>任务 {{ job.id }} · 已尝试 {{ job.processingAttempts }} 次</small>
            </div>
            <p>{{ job.errorMessage }}</p>
            <time>{{ formatTime(job.completedAt || job.createdAt) }}</time>
          </article>
          <div v-if="!jobsLoading && !jobRows.length" class="jobs-empty">
            该时间范围内没有失败的后台任务。
          </div>
        </div>
        <el-pagination
          v-model:current-page="jobFilter.page"
          v-model:page-size="jobFilter.pageSize"
          background
          layout="total, prev, pager, next"
          :total="jobTotal"
          @current-change="loadJobs"
        />
      </div>
    </section>
  </main>
</template>

<style scoped>
.operations-console {
  --ink: #1e2932;
  --muted: #66747e;
  --line: #d8dee1;
  --paper: #f6f8f8;
  --panel: #ffffff;
  --signal: #1f7468;
  --warning: #b47722;
  --danger: #b1463e;
  color: var(--ink);
  display: grid;
  gap: 18px;
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
  padding-bottom: 28px;
}

.operations-console > * {
  box-sizing: border-box;
  min-width: 0;
  width: 100%;
}

.console-heading,
.panel-heading,
.signal-board,
.console-split,
.backup-row,
.job-row {
  display: flex;
}

.console-heading {
  align-items: flex-end;
  justify-content: space-between;
  border-bottom: 1px solid var(--line);
  padding: 4px 2px 16px;
}

.swagger-panel {
  align-items: center;
  background: #fff;
  border: 1px solid var(--line);
  border-left: 4px solid var(--signal);
  display: flex;
  justify-content: space-between;
  gap: 18px;
  padding: 18px 20px;
}

.swagger-panel span {
  color: var(--signal);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
}

.swagger-panel h3 { margin: 4px 0; }
.swagger-panel p { color: var(--muted); margin: 0; }
.swagger-actions { align-items: center; display: flex; flex-wrap: wrap; gap: 12px; }

.console-kicker,
.panel-heading span,
.signal-summary > span,
.signal-node small,
.alert-row small,
.backup-row small,
.job-row small {
  color: #74818a;
  font-family: "Cascadia Mono", "SFMono-Regular", Consolas, monospace;
  font-size: 10px;
  letter-spacing: .09em;
  text-transform: uppercase;
}

.console-heading h2 {
  font-size: clamp(25px, 3vw, 38px);
  font-weight: 720;
  letter-spacing: -.04em;
  line-height: 1;
  margin: 8px 0 9px;
}

.console-heading p,
.signal-summary p,
.signal-node p,
.alert-row p,
.backup-rule,
.backup-copy p,
.job-row p,
.clear-state p {
  color: var(--muted);
  margin: 0;
}

.signal-board {
  background:
    linear-gradient(90deg, rgba(31, 116, 104, .06) 1px, transparent 1px) 0 0 / 42px 100%,
    var(--panel);
  border: 1px solid #cfd8d8;
  box-shadow: 0 12px 30px rgba(33, 51, 59, .06);
  min-height: 184px;
}

.signal-summary {
  background: #25323a;
  color: white;
  flex: 0 0 210px;
  padding: 25px;
}

.signal-summary > span { color: #aebdc2; }

.signal-summary strong {
  display: block;
  font-size: 34px;
  letter-spacing: -.05em;
  margin: 28px 0 9px;
}

.signal-summary p {
  color: #c2cdd0;
  font-size: 12px;
  line-height: 1.55;
}

.signal-track {
  display: grid;
  flex: 1;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  padding: 22px 18px;
}

.signal-node {
  min-width: 0;
  padding: 4px 16px 2px;
  position: relative;
}

.signal-node:not(:last-child)::after {
  background: var(--line);
  content: "";
  height: 1px;
  left: 43px;
  position: absolute;
  right: -16px;
  top: 16px;
}

.signal-mark {
  align-items: center;
  background: var(--panel);
  display: flex;
  height: 28px;
  margin-bottom: 19px;
  position: relative;
  width: 28px;
  z-index: 1;
}

.signal-mark span {
  align-items: center;
  background: #e7eeed;
  border: 1px solid #b8c8c5;
  border-radius: 50%;
  color: var(--signal);
  display: flex;
  font-family: "Cascadia Mono", Consolas, monospace;
  font-size: 10px;
  height: 24px;
  justify-content: center;
  width: 24px;
}

.signal-node.is-warning .signal-mark span { background: #fbf0dd; border-color: #dfc092; color: var(--warning); }
.signal-node.is-unhealthy .signal-mark span { background: #fae7e5; border-color: #d7aaa6; color: var(--danger); }

.signal-node h3 {
  font-size: 15px;
  margin: 4px 0 8px;
}

.signal-node p {
  font-size: 11px;
  line-height: 1.5;
  min-height: 48px;
}

.signal-node > b {
  color: var(--signal);
  display: block;
  font-size: 11px;
  margin-top: 10px;
}

.is-warning { color: var(--warning) !important; }
.is-unhealthy { color: var(--danger) !important; }
.is-healthy { color: var(--signal) !important; }

.console-split {
  align-items: stretch;
  gap: 18px;
}

.alerts-panel,
.backup-panel,
.ledger-panel {
  background: var(--panel);
  border: 1px solid var(--line);
}

.alerts-panel { flex: 0 0 min(38%, 460px); }
.backup-panel { flex: 1; min-width: 0; }

.panel-heading {
  align-items: center;
  border-bottom: 1px solid var(--line);
  justify-content: space-between;
  min-height: 72px;
  padding: 14px 18px;
}

.panel-heading h3 {
  font-size: 17px;
  margin: 3px 0 0;
}

.panel-heading em {
  align-items: center;
  background: #f3e3e1;
  border-radius: 50%;
  color: var(--danger);
  display: flex;
  font-size: 12px;
  font-style: normal;
  height: 30px;
  justify-content: center;
  width: 30px;
}

.alert-list { max-height: 365px; overflow: auto; }

.alert-row {
  align-items: flex-start;
  border-bottom: 1px solid #e7ebed;
  gap: 12px;
  padding: 15px 18px;
}

.alert-row:last-child { border-bottom: 0; }
.alert-row > .el-icon { color: var(--warning); font-size: 17px; margin-top: 2px; }
.alert-row.is-critical > .el-icon { color: var(--danger); }
.alert-row strong { display: block; font-size: 13px; line-height: 1.45; }
.alert-row p { font-size: 11px; line-height: 1.55; margin: 4px 0 7px; }

.clear-state {
  align-items: flex-start;
  display: flex;
  gap: 13px;
  padding: 30px 20px;
}

.clear-state .el-icon { color: var(--signal); font-size: 22px; }
.clear-state strong { font-size: 14px; }
.clear-state p { font-size: 12px; margin-top: 5px; }

.backup-rule {
  background: #f5f8f7;
  border-bottom: 1px solid #e0e6e5;
  font-size: 11px;
  padding: 10px 18px;
}

.backup-list { max-height: 324px; overflow: auto; }

.backup-row {
  align-items: center;
  border-bottom: 1px solid #e7ebed;
  gap: 14px;
  padding: 13px 18px;
}

.backup-row:last-child { border-bottom: 0; }

.backup-stamp {
  border-right: 1px solid var(--line);
  flex: 0 0 78px;
  padding-right: 13px;
}

.backup-stamp span {
  color: var(--signal);
  display: block;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
}

.backup-stamp b { display: block; font-size: 12px; margin-top: 5px; }
.backup-copy { flex: 1; min-width: 0; }
.backup-copy strong { display: block; font-family: "Cascadia Mono", Consolas, monospace; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.backup-copy p { font-size: 11px; margin: 5px 0; }
.backup-copy small { font-size: 9px; }
.drill-state { flex: 0 0 112px; text-align: right; }
.drill-state > span { display: block; font-size: 10px; font-weight: 700; margin-bottom: 3px; }
.drill-state .passed { color: var(--signal); }
.drill-state .failed { color: var(--danger); }
.drill-state .pending { color: var(--warning); }
.empty-backup { color: var(--muted); font-size: 12px; padding: 30px 18px; }

.ledger-tabs {
  border-bottom: 1px solid var(--line);
  display: flex;
  gap: 26px;
  padding: 0 20px;
}

.ledger-tabs button {
  background: transparent;
  border: 0;
  color: var(--muted);
  cursor: pointer;
  font-size: 14px;
  font-weight: 650;
  padding: 18px 0 14px;
  position: relative;
}

.ledger-tabs button::after {
  background: var(--signal);
  bottom: -1px;
  content: "";
  height: 2px;
  left: 0;
  position: absolute;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .18s ease;
  width: 100%;
}

.ledger-tabs button.active { color: var(--ink); }
.ledger-tabs button.active::after { transform: scaleX(1); }
.ledger-tabs span { background: #edf1f1; border-radius: 10px; font-size: 10px; margin-left: 6px; padding: 2px 6px; }
.ledger-content { padding: 16px 18px 18px; }

.query-strip {
  display: grid;
  gap: 9px;
  grid-template-columns: 120px 130px 150px minmax(170px, 1fr) 360px auto auto;
  margin-bottom: 14px;
}

.jobs-query { grid-template-columns: 180px 360px auto; }
.method-pill { color: #355b65; font-family: "Cascadia Mono", Consolas, monospace; font-size: 11px; font-weight: 700; }
.el-pagination { justify-content: flex-end; margin-top: 16px; }

.job-list { min-height: 180px; }

.job-row {
  align-items: flex-start;
  border-bottom: 1px solid #e3e8e9;
  gap: 22px;
  padding: 15px 4px;
}

.job-row > div { flex: 0 0 260px; }
.job-row span { color: var(--danger); display: block; font-size: 10px; font-weight: 750; letter-spacing: .06em; }
.job-row strong { display: block; font-size: 13px; margin: 5px 0; }
.job-row small { display: block; font-size: 9px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.job-row p { flex: 1; font-size: 12px; line-height: 1.6; }
.job-row time { color: var(--muted); flex: 0 0 150px; font-family: "Cascadia Mono", Consolas, monospace; font-size: 10px; text-align: right; }
.jobs-empty { color: var(--muted); font-size: 12px; padding: 34px 4px; text-align: center; }

@media (prefers-reduced-motion: reduce) {
  .ledger-tabs button::after { transition: none; }
}

@media (max-width: 1180px) {
  .signal-track { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 18px; }
  .signal-node:nth-child(2)::after { display: none; }
  .query-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .query-strip .el-date-editor { grid-column: span 2; width: 100%; }
  .jobs-query { grid-template-columns: 180px minmax(300px, 1fr) auto; }
}

@media (max-width: 820px) {
  .signal-board,
  .console-split { display: block; }
  .signal-summary { padding: 20px; }
  .signal-summary strong { margin-top: 16px; }
  .signal-track { padding: 18px 8px; }
  .alerts-panel { margin-bottom: 18px; }
  .job-row { display: grid; gap: 8px; grid-template-columns: 1fr auto; }
  .job-row > div,
  .job-row p,
  .job-row time { grid-column: 1 / -1; text-align: left; }
}

@media (max-width: 560px) {
  .operations-console { gap: 12px; }
  .console-heading { align-items: flex-start; gap: 14px; }
  .swagger-panel { align-items: flex-start; flex-direction: column; }
  .console-heading p { font-size: 12px; line-height: 1.5; }
  .signal-track { display: block; }
  .signal-node { border-bottom: 1px solid #e4e9ea; padding: 13px 12px; }
  .signal-node:last-child { border-bottom: 0; }
  .signal-node::after { display: none !important; }
  .signal-mark { float: left; margin: 0 12px 26px 0; }
  .signal-node p { min-height: 0; }
  .panel-heading { align-items: flex-start; gap: 12px; }
  .backup-row { align-items: flex-start; flex-wrap: wrap; }
  .backup-copy { min-width: calc(100% - 100px); }
  .drill-state { display: flex; flex: 1 0 100%; justify-content: space-between; text-align: left; }
  .query-strip,
  .jobs-query { display: flex; flex-wrap: wrap; }
  .query-strip > * { flex: 1 1 140px; }
  .query-strip .el-date-editor { flex-basis: 100%; max-width: 100%; width: 100%; }
  .ledger-panel,
  .ledger-content { min-width: 0; overflow: hidden; }
  .ledger-content { padding: 12px; }
  .el-pagination { justify-content: center; overflow: auto; }
}
</style>
