feat: UI Problems list filters, detail card and event timeline (d2-1)

- Filters status/severity/hostname; /problems/:id with Ack/Resolve and timeline

- Dashboard drill-down to open problems

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-28 09:55:28 +10:00
parent 64b5ef297a
commit 249894d8e1
7 changed files with 206 additions and 9 deletions
+135
View File
@@ -0,0 +1,135 @@
<template>
<p><RouterLink to="/problems"> Проблемы</RouterLink></p>
<p v-if="error" class="error">{{ error }}</p>
<p v-else-if="loading">Загрузка</p>
<template v-else-if="problem">
<h1>{{ problem.title }}</h1>
<div class="card report-meta">
<p>
<strong>ID:</strong> {{ problem.id }}
· <strong>Status:</strong>
<span :class="'status-' + problem.status">{{ problem.status }}</span>
· <strong>Severity:</strong>
<span :class="'sev-' + problem.severity">{{ problem.severity }}</span>
</p>
<p>
<strong>Хост:</strong>
<RouterLink
v-if="problem.hostname"
:to="{ path: '/reports', query: { hostname: problem.hostname } }"
>
{{ problem.hostname }}
</RouterLink>
<span v-else></span>
· <strong>Rule:</strong> <code>{{ problem.rule_id ?? "—" }}</code>
</p>
<p><strong>Summary:</strong> {{ problem.summary }}</p>
<p>
<strong>Событий:</strong> {{ problem.event_count ?? "—" }}
· <strong>Last seen:</strong> {{ formatDt(problem.last_seen_at ?? problem.updated_at) }}
· <strong>Обновлено:</strong> {{ formatDt(problem.updated_at) }}
</p>
<p v-if="problem.fingerprint">
<strong>Fingerprint:</strong> <code>{{ problem.fingerprint }}</code>
</p>
<p class="actions">
<button
v-if="problem.status === 'open'"
type="button"
class="secondary"
:disabled="acting"
@click="doAck"
>
Ack
</button>
<button v-if="problem.status !== 'resolved'" type="button" :disabled="acting" @click="doResolve">
Resolve
</button>
</p>
</div>
<h2>Таймлайн событий</h2>
<p v-if="!problem.events.length">Нет связанных событий.</p>
<table v-else class="timeline-table">
<thead>
<tr>
<th>Время</th>
<th>Severity</th>
<th>Type</th>
<th>Title</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<tr v-for="e in problem.events" :key="e.id">
<td>{{ formatDt(e.occurred_at) }}</td>
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
<td>
<RouterLink :to="`/events/${e.id}`"><code>{{ e.type }}</code></RouterLink>
</td>
<td>{{ e.title }}</td>
<td>{{ e.summary }}</td>
</tr>
</tbody>
</table>
</template>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import { ackProblem, fetchProblem, resolveProblem, type ProblemDetail } from "../api";
const props = defineProps<{ id: string }>();
const problem = ref<ProblemDetail | null>(null);
const loading = ref(false);
const error = ref("");
const acting = ref(false);
function formatDt(iso: string) {
return new Date(iso).toLocaleString("ru-RU");
}
async function load() {
loading.value = true;
error.value = "";
try {
problem.value = await fetchProblem(Number(props.id));
} catch (e) {
error.value = e instanceof Error ? e.message : "Не найдено";
} finally {
loading.value = false;
}
}
async function doAck() {
if (!problem.value) return;
acting.value = true;
error.value = "";
try {
await ackProblem(problem.value.id);
await load();
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка ack";
} finally {
acting.value = false;
}
}
async function doResolve() {
if (!problem.value) return;
acting.value = true;
error.value = "";
try {
await resolveProblem(problem.value.id);
await load();
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка resolve";
} finally {
acting.value = false;
}
}
onMounted(load);
watch(() => props.id, load);
</script>