diff --git a/app/components/Ui/Card/Action.vue b/app/components/Ui/Card/Action.vue new file mode 100644 index 0000000..a0d5c29 --- /dev/null +++ b/app/components/Ui/Card/Action.vue @@ -0,0 +1,31 @@ + + + diff --git a/app/components/Ui/Card/Card.vue b/app/components/Ui/Card/Card.vue new file mode 100644 index 0000000..58f6b26 --- /dev/null +++ b/app/components/Ui/Card/Card.vue @@ -0,0 +1,58 @@ + + + diff --git a/app/components/Ui/Card/Content.vue b/app/components/Ui/Card/Content.vue new file mode 100644 index 0000000..222c8fe --- /dev/null +++ b/app/components/Ui/Card/Content.vue @@ -0,0 +1,34 @@ + + + diff --git a/app/components/Ui/Card/Description.vue b/app/components/Ui/Card/Description.vue new file mode 100644 index 0000000..7ead06e --- /dev/null +++ b/app/components/Ui/Card/Description.vue @@ -0,0 +1,36 @@ + + + diff --git a/app/components/Ui/Card/Footer.vue b/app/components/Ui/Card/Footer.vue new file mode 100644 index 0000000..282f509 --- /dev/null +++ b/app/components/Ui/Card/Footer.vue @@ -0,0 +1,31 @@ + + + diff --git a/app/components/Ui/Card/Header.vue b/app/components/Ui/Card/Header.vue new file mode 100644 index 0000000..e883e5a --- /dev/null +++ b/app/components/Ui/Card/Header.vue @@ -0,0 +1,31 @@ + + + diff --git a/app/components/Ui/Card/Title.vue b/app/components/Ui/Card/Title.vue new file mode 100644 index 0000000..ee8ac47 --- /dev/null +++ b/app/components/Ui/Card/Title.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/components/Ui/Progress/Indicator.vue b/app/components/Ui/Progress/Indicator.vue new file mode 100644 index 0000000..fe6813b --- /dev/null +++ b/app/components/Ui/Progress/Indicator.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/components/Ui/Progress/Progress.vue b/app/components/Ui/Progress/Progress.vue new file mode 100644 index 0000000..67f16da --- /dev/null +++ b/app/components/Ui/Progress/Progress.vue @@ -0,0 +1,41 @@ + + + diff --git a/app/components/dash/Failures.vue b/app/components/dash/Failures.vue new file mode 100644 index 0000000..41c149c --- /dev/null +++ b/app/components/dash/Failures.vue @@ -0,0 +1,105 @@ + + + diff --git a/app/components/dash/Groups.vue b/app/components/dash/Groups.vue new file mode 100644 index 0000000..06ca5bf --- /dev/null +++ b/app/components/dash/Groups.vue @@ -0,0 +1,185 @@ + + + diff --git a/app/components/dash/Hero.vue b/app/components/dash/Hero.vue new file mode 100644 index 0000000..56b32e0 --- /dev/null +++ b/app/components/dash/Hero.vue @@ -0,0 +1,153 @@ + + + diff --git a/app/components/dash/RunItem.vue b/app/components/dash/RunItem.vue new file mode 100644 index 0000000..e731537 --- /dev/null +++ b/app/components/dash/RunItem.vue @@ -0,0 +1,334 @@ + + + diff --git a/app/components/dash/TableSizes.vue b/app/components/dash/TableSizes.vue new file mode 100644 index 0000000..6f3dc6c --- /dev/null +++ b/app/components/dash/TableSizes.vue @@ -0,0 +1,74 @@ + + + diff --git a/app/composables/useDash.ts b/app/composables/useDash.ts new file mode 100644 index 0000000..5d0107b --- /dev/null +++ b/app/composables/useDash.ts @@ -0,0 +1,127 @@ +import type { SyncRun, SyncRunGroup, DashSummary } from "~/queries/dash" + +// ── Labels ──────────────────────────────────────────────────────────────────── + +export const STATUS_LABELS: Record = { + running: "Виконується", + success: "Успіх", + partial: "Частковий", + failed: "Помилка", + unknown: "Невідомо", +} + +export const TRIGGER_LABELS: Record = { + cron: "Розклад", + bootstrap: "Запуск", +} + +export const STEP_LABELS: Record = { + auditoriums: "Аудиторії", + groups: "Групи", + teachers: "Викладачі", +} + +// ── Status variant ───────────────────────────────────────────────────────────── + +export function statusVariant( + status: SyncRun["status"] | DashSummary["currentStatus"] +): "success" | "warning" | "destructive" | "secondary" | "info" { + switch (status) { + case "success": + return "success" + case "partial": + return "warning" + case "failed": + return "destructive" + case "running": + return "info" + default: + return "secondary" + } +} + +// ── Formatting ──────────────────────────────────────────────────────────────── + +/** dd.MM HH:mm — for collapsed run rows */ +export function fmtShort(iso: string | null | undefined): string { + if (!iso) return "—" + return new Date(iso).toLocaleString("uk-UA", { + day: "2-digit", + month: "2-digit", + hour: "2-digit", + minute: "2-digit", + }) +} + +export function fmt(iso: string | null | undefined): string { + if (!iso) return "—" + return new Date(iso).toLocaleString("uk-UA", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }) +} + +export function fmtRelative(iso: string | null | undefined): string { + if (!iso) return "—" + const ms = Date.now() - new Date(iso).getTime() + const s = Math.floor(ms / 1000) + const m = Math.floor(s / 60) + const h = Math.floor(m / 60) + const d = Math.floor(h / 24) + if (d > 0) return `${d} дн тому` + if (h > 0) return `${h} год тому` + if (m > 0) return `${m} хв тому` + return `${s}с тому` +} + +export function duration(start: string, end: string | null): string { + if (!end) return "—" + const ms = new Date(end).getTime() - new Date(start).getTime() + const s = Math.floor(ms / 1000) + const m = Math.floor(s / 60) + const h = Math.floor(m / 60) + if (h > 0) return `${h}г ${m % 60}хв` + if (m > 0) return `${m}хв ${s % 60}с` + return `${s}с` +} + +/** Duration in ms for sparkbar scaling */ +export function durationMs(start: string, end: string | null): number { + if (!end) return 0 + return new Date(end).getTime() - new Date(start).getTime() +} + +// ── Events delta ─────────────────────────────────────────────────────────────── + +export function deltaLabel(curr: number, prev: number | null): string { + if (prev === null) return "" + const diff = curr - prev + if (diff === 0) return "=" + return diff > 0 ? `+${diff}` : String(diff) +} + +export function deltaClass(curr: number, prev: number | null): string { + if (prev === null) return "text-muted-foreground" + const diff = curr - prev + if (diff === 0) return "text-muted-foreground" + return diff > 0 ? "text-green-600 dark:text-green-400" : "text-destructive" +} + +// ── Spike detection ─────────────────────────────────────────────────────────── + +const REMOVED_SPIKE_RATIO = 0.05 + +export function isRemovedSpike(run: SyncRun): boolean { + if (run.totalEvents === 0) return false + return run.removedEvents / run.totalEvents > REMOVED_SPIKE_RATIO +} + +// ── Group label ─────────────────────────────────────────────────────────────── + +export function groupLabel(g: Pick): string { + return g.groupName ?? `#${g.groupId}` +} diff --git a/app/layouts/dash.vue b/app/layouts/dash.vue index 312873a..b7d269d 100644 --- a/app/layouts/dash.vue +++ b/app/layouts/dash.vue @@ -1,5 +1,5 @@