feat: v0.2.0 branding, healthz version, SSE dashboard live

Centralize APP_VERSION 0.2.0; /health and /healthz return version;
startup log line; UI title Security Alert Center v.0.2.0;
SSE /api/v1/stream/events for live dashboard counters.
This commit is contained in:
2026-05-27 13:20:39 +10:00
parent ea221db3c7
commit c657a65970
15 changed files with 197 additions and 25 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "sac-ui",
"private": true,
"version": "0.1.0",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",
+4 -1
View File
@@ -1,7 +1,7 @@
<template>
<div class="layout">
<nav v-if="showNav">
<span class="brand">SAC</span>
<span class="brand">{{ appTitle }}</span>
<RouterLink to="/dashboard">Dashboard</RouterLink>
<RouterLink to="/events">События</RouterLink>
<RouterLink to="/problems">Проблемы</RouterLink>
@@ -16,6 +16,9 @@
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { clearToken, getToken } from "./api";
import { APP_VERSION_LABEL } from "./version";
const appTitle = APP_VERSION_LABEL;
const route = useRoute();
const router = useRouter();
+11
View File
@@ -36,6 +36,9 @@ nav .brand {
font-weight: 700;
color: #fff;
margin-right: auto;
font-size: 0.95rem;
line-height: 1.3;
max-width: min(420px, 45vw);
}
table {
@@ -164,3 +167,11 @@ pre {
flex-wrap: wrap;
gap: 1rem;
}
.live-badge {
color: #6bcb77;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
+3
View File
@@ -0,0 +1,3 @@
export const APP_NAME = "Security Alert Center";
export const APP_VERSION = "0.2.0";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
+45 -3
View File
@@ -54,17 +54,20 @@
</table>
<div class="filters" style="margin-top: 1rem">
<button type="button" class="secondary" @click="load">Обновить</button>
<span v-if="live" class="live-badge">live</span>
</div>
</template>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { apiFetch, type DashboardSummary } from "../api";
import { onMounted, onUnmounted, ref } from "vue";
import { apiFetch, getToken, type DashboardSummary } from "../api";
const data = ref<DashboardSummary | null>(null);
const loading = ref(false);
const error = ref("");
const live = ref(false);
let eventSource: EventSource | null = null;
function formatDt(iso: string) {
return new Date(iso).toLocaleString("ru-RU");
@@ -82,5 +85,44 @@ async function load() {
}
}
onMounted(() => load());
function connectLive() {
const token = getToken();
if (!token) return;
eventSource?.close();
const url = `/api/v1/stream/events?token=${encodeURIComponent(token)}`;
eventSource = new EventSource(url);
eventSource.onopen = () => {
live.value = true;
};
eventSource.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data) as {
type?: string;
events_last_24h?: number;
problems_open?: number;
};
if (msg.type !== "dashboard" || !data.value) return;
if (typeof msg.events_last_24h === "number") {
data.value.events_last_24h = msg.events_last_24h;
}
if (typeof msg.problems_open === "number") {
data.value.problems_open = msg.problems_open;
}
} catch {
/* ignore malformed SSE */
}
};
eventSource.onerror = () => {
live.value = false;
};
}
onMounted(() => {
load().then(() => connectLive());
});
onUnmounted(() => {
eventSource?.close();
eventSource = null;
});
</script>
+1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
proxy: {
"/api": "http://127.0.0.1:8000",
"/health": "http://127.0.0.1:8000",
"/healthz": "http://127.0.0.1:8000",
},
},
build: {