diff --git a/app/pages/dash.vue b/app/pages/dash.vue
index ab5e326..d0c2856 100644
--- a/app/pages/dash.vue
+++ b/app/pages/dash.vue
@@ -1,501 +1,219 @@
-
-
-
-
Schedule Dashboard
-
-
-
-
-
- {{ error }}
-
-
-
-
-
-
Статус
-
- {{ summary.currentStatus }}
-
-
-
-
-
Прогрес
-
-
- {{ summary.progress.current }} / {{ summary.progress.total }} груп
-
- —
-
-
-
-
-
Останній успіх
-
- {{ fmt(summary.lastSuccessfulRun?.finishedAt) }}
-
-
-
-
-
Наступний запуск (cron)
-
{{ fmt(summary.nextCronAt) }}
-
-
-
-
-
-
-
Останні запуски
-
-
-
-
- ID
- Статус
- Тригер
- Початок
- Тривалість
- Групи (провал)
- Подій всього
- Видалено
- Кроки
-
-
-
-
- Немає даних
-
-
- {{ run.id }}
-
-
-
- {{ run.status }}
-
-
- {{ run.trigger }}
- {{ fmt(run.startedAt) }}
-
- {{ duration(run.startedAt, run.finishedAt) }}
-
-
- {{ run.totalGroups }}
-
- ({{ run.failedGroups }} ✗)
-
-
- {{ run.totalEvents }}
-
-
- {{ run.removedEvents }}
-
- ⚠
-
-
-
-
-
- {{ String(name)[0].toUpperCase() }}:{{ step.count
- }}{{ step.ok ? "" : "✗" }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Групи — запуск {{ selectedRunId }}
-
-
-
- Завантаження…
-
-
-
-
- Група ID
- Статус
- Подій
- Δ
- Час
- Помилка
-
-
-
-
- Немає даних
-
-
- {{ g.groupId }}
-
-
- {{ g.status }}
-
-
- {{ g.eventsCount }}
- Прогони
+
+ Провали
+ {{ failureCount }}
- {{ deltaLabel(g.eventsCount, g.prevEventsCount) || "—" }}
-
- {{ fmt(g.finishedAt) }}
-
- {{ g.error ?? "—" }}
-
-
-
-
-
+
+
Групи
+
База даних
+
+
+
+
+
+
+
+
Статус:
+
+
+
+
+
+ Всі
+ Успіх
+ Частковий
+ Помилка
+ Виконується
+
+
+
+
+
+
+
+
-
-
-
-
- Останні помилки груп
-
- (останні 50)
-
-
-
-
-
-
- Запуск ID
- Група ID
- Час
- Помилка
-
-
-
-
- {{ f.runId }}
- {{ f.groupId }}
- {{ fmt(f.finishedAt) }}
-
- {{ f.error ?? "—" }}
-
-
-
-
-
+
+
+
-
-
-
-
Розмір таблиць БД
-
-
-
-
- Таблиця
- Рядків
- Розмір
-
-
-
-
- {{ t.tableName }}
-
- {{ t.rowCount.toLocaleString("uk-UA") }}
-
- {{ t.sizePretty }}
-
-
-
-
+
+
+
+
+ Початок
+
+ Тривалість
+ Тригер
+ Групи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/queries/dash.ts b/app/queries/dash.ts
new file mode 100644
index 0000000..729ebec
--- /dev/null
+++ b/app/queries/dash.ts
@@ -0,0 +1,136 @@
+import { queryOptions } from "@tanstack/vue-query"
+import type { MaybeRef } from "vue"
+
+// ── Types ────────────────────────────────────────────────────────────────────
+
+export interface StepResult {
+ ok: boolean
+ count: number
+ error?: string
+}
+
+export interface SyncSteps {
+ auditoriums: StepResult
+ groups: StepResult
+ teachers: StepResult
+}
+
+export interface SyncRun {
+ id: number
+ startedAt: string
+ finishedAt: string | null
+ status: "running" | "success" | "partial" | "failed"
+ trigger: "cron" | "bootstrap"
+ totalGroups: number
+ failedGroups: number
+ removedEvents: number
+ totalEvents: number
+ steps: SyncSteps
+}
+
+export interface SyncRunGroup {
+ runId: number
+ groupId: number
+ groupName: string | null
+ status: "success" | "failed"
+ eventsCount: number
+ prevEventsCount: number | null
+ error: string | null
+ finishedAt: string
+}
+
+export interface FailedGroupEntry {
+ runId: number
+ groupId: number
+ groupName: string | null
+ error: string | null
+ finishedAt: string
+}
+
+export interface TableSizeEntry {
+ tableName: string
+ rowCount: number
+ sizePretty: string
+ sizeBytes: number
+}
+
+export interface DashSummary {
+ currentStatus: "running" | "success" | "partial" | "failed" | "unknown"
+ isRunning: boolean
+ lastRun: SyncRun | null
+ lastSuccessfulRun: SyncRun | null
+ nextCronAt: string | null
+ totalRuns: number
+ progress: { current: number; total: number } | null
+}
+
+interface ApiResponse
{
+ success: boolean
+ data: T
+ error: null | { code: string; message: string }
+}
+
+// ── Factories ─────────────────────────────────────────────────────────────────
+
+const BASE_OPTIONS = {
+ // Ops data — never persist to IndexedDB
+ gcTime: 0,
+ staleTime: 0,
+} as const
+
+export function dashSummaryOptions(isRunning: MaybeRef) {
+ return queryOptions({
+ ...BASE_OPTIONS,
+ queryKey: ["dash", "summary"] as const,
+ queryFn: () => $fetch>("/dash-api/summary").then((r) => r.data),
+ refetchInterval: () => (toValue(isRunning) ? 5_000 : 60_000),
+ })
+}
+
+export function dashRunsOptions(limit = 30) {
+ return queryOptions({
+ ...BASE_OPTIONS,
+ queryKey: ["dash", "runs", limit] as const,
+ queryFn: () =>
+ $fetch>(`/dash-api/runs?limit=${limit}`).then(
+ (r) => r.data ?? []
+ ),
+ refetchInterval: 60_000,
+ })
+}
+
+export function dashRunGroupsOptions(runId: MaybeRef) {
+ return queryOptions({
+ ...BASE_OPTIONS,
+ queryKey: computed(() => ["dash", "run-groups", toValue(runId)]),
+ queryFn: () =>
+ $fetch>(`/dash-api/runs/${toValue(runId)}/groups`).then(
+ (r) => r.data ?? []
+ ),
+ enabled: computed(() => toValue(runId) !== null),
+ })
+}
+
+export function dashFailuresOptions(limit = 50) {
+ return queryOptions({
+ ...BASE_OPTIONS,
+ queryKey: ["dash", "failures", limit] as const,
+ queryFn: () =>
+ $fetch>(`/dash-api/failures?limit=${limit}`).then(
+ (r) => r.data ?? []
+ ),
+ refetchInterval: 60_000,
+ })
+}
+
+export function dashTableSizesOptions() {
+ return queryOptions({
+ ...BASE_OPTIONS,
+ queryKey: ["dash", "table-sizes"] as const,
+ queryFn: () =>
+ $fetch>("/dash-api/table-sizes").then(
+ (r) => r.data ?? []
+ ),
+ refetchInterval: 5 * 60_000,
+ })
+}
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 637f663..3a06d65 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -20,6 +20,7 @@ export default defineNuxtConfig({
{ path: "~/components/schedule", prefix: "Schedule", pathPrefix: false },
{ path: "~/components/filters", prefix: "Filters", pathPrefix: false },
{ path: "~/components/links", prefix: "Links", pathPrefix: false },
+ { path: "~/components/dash", prefix: "Dash", pathPrefix: false },
{ path: "~/components", pathPrefix: false, extensions: [".vue"] },
],
imports: {
diff --git a/package.json b/package.json
index ecf5a4b..52ee446 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "mindenit-schedule",
- "version": "1.2.0",
+ "version": "1.2.1",
"private": true,
"type": "module",
"scripts": {