From 2e9b4787b4dbcf5b3456d9114fdf14c06bd03701 Mon Sep 17 00:00:00 2001
From: Roman Trashutin
Date: Tue, 1 Sep 2026 16:32:25 +0300
Subject: [PATCH 01/10] feat(dashboard): add /dash page with sync history and
proxy route
- server/routes/dash-api/[...].ts: nitro proxy to backend /api/dash/*
- NUXT_DASH_API_BASE_URL private runtimeConfig (default localhost:8080)
- routeRules: /dash and /dash-api/** excluded from robots/sitemap, ssr:false
- pages/dash.vue: status cards, runs table, per-run group breakdown
- polls every 5s while running, 60s idle
- UiBadge variants for all statuses
- without-navbar layout
---
.env.example | 5 +
app/pages/dash.vue | 350 ++++++++++++++++++++++++++++++++
nuxt.config.ts | 7 +
server/routes/dash-api/[...].ts | 21 ++
4 files changed, 383 insertions(+)
create mode 100644 app/pages/dash.vue
create mode 100644 server/routes/dash-api/[...].ts
diff --git a/.env.example b/.env.example
index d9df30a..a16ad8c 100644
--- a/.env.example
+++ b/.env.example
@@ -13,3 +13,8 @@ NUXT_OG_IMAGE_SECRET=
# Get it from: GA4 property → Admin → Data Streams → Web stream → Measurement ID.
# Leave empty to disable analytics entirely (no script loaded, no consent banner).
NUXT_PUBLIC_GA_MEASUREMENT_ID=
+
+# Backend URL for the /dash-api proxy (server-side only, never sent to browser).
+# In production, point this to the internal API address (e.g. http://schedule-api:8080).
+# Default: http://localhost:8080
+NUXT_DASH_API_BASE_URL=
diff --git a/app/pages/dash.vue b/app/pages/dash.vue
new file mode 100644
index 0000000..22d0bb6
--- /dev/null
+++ b/app/pages/dash.vue
@@ -0,0 +1,350 @@
+
+
+
+
+
+
+
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.removedEvents }}
+
+
+ {{ name[0].toUpperCase() }}:{{ step.count }}{{ step.ok ? "" : "✗" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Групи — запуск {{ selectedRunId }}
+
+
+
+ Завантаження…
+
+
+
+
+ Група ID
+ Статус
+ Подій
+ Час
+ Помилка
+
+
+
+
+ Немає даних
+
+
+ {{ g.groupId }}
+
+
+ {{ g.status }}
+
+
+ {{ g.eventsCount }}
+ {{ fmt(g.finishedAt) }}
+
+ {{ g.error ?? "—" }}
+
+
+
+
+
+
+
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 8e09318..19146e4 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -35,6 +35,9 @@ export default defineNuxtConfig({
],
},
runtimeConfig: {
+ // Backend URL for the dashboard proxy — server-side only, never exposed to client.
+ // Override via NUXT_DASH_API_BASE_URL env var.
+ dashApiBaseUrl: "http://localhost:8080",
public: {
maintenance: false,
// Bumped on every build — used to bust the IndexedDB query cache on deploy
@@ -131,6 +134,10 @@ export default defineNuxtConfig({
// check the SW byte-for-byte on every page load, but a stale cached response
// would prevent the browser from seeing an updated worker.
"/sw.js": { headers: { "cache-control": "no-cache, no-store, must-revalidate" } },
+ // Dashboard — internal ops page, no robots, no SSR, no caching.
+ "/dash": { robots: false, sitemap: false, ssr: false },
+ // Proxy route for dashboard API — same auth boundary as /dash.
+ "/dash-api/**": { robots: false },
},
robots: {
sitemap: "/sitemap.xml",
diff --git a/server/routes/dash-api/[...].ts b/server/routes/dash-api/[...].ts
new file mode 100644
index 0000000..17b537f
--- /dev/null
+++ b/server/routes/dash-api/[...].ts
@@ -0,0 +1,21 @@
+/**
+ * Server-side proxy for the internal dashboard API.
+ *
+ * Requests to /dash-api/* are forwarded to the backend API server at
+ * NUXT_DASH_API_BASE_URL/api/dash/*. The base URL stays server-side only —
+ * the browser never sees it, which is what lets Caddy guard /dash-api/* with
+ * basic_auth at the edge without exposing the backend origin.
+ *
+ * Default base URL points to the same host the schedule backend runs on in
+ * local dev (http://localhost:8080). Override via NUXT_DASH_API_BASE_URL env.
+ */
+export default defineEventHandler(async (event) => {
+ const config = useRuntimeConfig()
+ const base = (config.dashApiBaseUrl as string | undefined) || "http://localhost:8080"
+
+ // Strip /dash-api prefix, keep the rest (e.g. /summary, /runs, /runs/123/groups)
+ const path = event.path.replace(/^\/dash-api/, "") || "/"
+ const target = `${base.replace(/\/$/, "")}/api/dash${path}`
+
+ return proxyRequest(event, target)
+})
From 653e5bf7390113ca82bc46e9ab725f601b00d811 Mon Sep 17 00:00:00 2001
From: Roman Trashutin
Date: Tue, 1 Sep 2026 17:06:42 +0300
Subject: [PATCH 02/10] fix(dashboard): guard runs and selectedRunGroups
against null api response
---
app/pages/dash.vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/pages/dash.vue b/app/pages/dash.vue
index 22d0bb6..86282df 100644
--- a/app/pages/dash.vue
+++ b/app/pages/dash.vue
@@ -80,7 +80,7 @@ async function fetchSummary() {
async function fetchRuns() {
try {
const res = await $fetch>("/dash-api/runs?limit=30")
- runs.value = res.data
+ runs.value = res.data ?? []
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
}
@@ -90,7 +90,7 @@ async function fetchGroups(runId: number) {
loadingGroups.value = true
try {
const res = await $fetch>(`/dash-api/runs/${runId}/groups`)
- selectedRunGroups.value = res.data
+ selectedRunGroups.value = res.data ?? []
selectedRunId.value = runId
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
From bd65b0960075c42a12a9709af469ae37164a0bbf Mon Sep 17 00:00:00 2001
From: Roman Trashutin
Date: Tue, 1 Sep 2026 17:07:47 +0300
Subject: [PATCH 03/10] fix(dashboard): replace useSeo with useHead to avoid
defineOgImage in client-only context
---
app/pages/dash.vue | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/pages/dash.vue b/app/pages/dash.vue
index 86282df..98157e7 100644
--- a/app/pages/dash.vue
+++ b/app/pages/dash.vue
@@ -3,7 +3,8 @@ definePageMeta({
layout: "without-navbar",
})
-useSeo({ title: "Dashboard", noindex: true })
+useHead({ title: "Dashboard" })
+useServerSeoMeta({ robots: "noindex, nofollow" })
// ── Types ────────────────────────────────────────────────────────────────────
From 8a4e72f85504b9112f2311359ab9797b6cd47ad7 Mon Sep 17 00:00:00 2001
From: Roman Trashutin
Date: Tue, 1 Sep 2026 17:09:41 +0300
Subject: [PATCH 04/10] fix(dashboard): swap UiTableHead/UiTableHeader usage to
fix stacked headers
---
app/pages/dash.vue | 36 ++++++++++++++++++------------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/app/pages/dash.vue b/app/pages/dash.vue
index 98157e7..8d85b88 100644
--- a/app/pages/dash.vue
+++ b/app/pages/dash.vue
@@ -234,19 +234,19 @@ function duration(start: string, end: string | null): string {
Останні запуски
-
+
- ID
- Статус
- Тригер
- Початок
- Тривалість
- Групи (провал)
- Видалено подій
- Кроки
-
+ ID
+ Статус
+ Тригер
+ Початок
+ Тривалість
+ Групи (провал)
+ Видалено подій
+ Кроки
+
-
+
Немає даних
-
+
- Група ID
- Статус
- Подій
- Час
- Помилка
+ Група ID
+ Статус
+ Подій
+ Час
+ Помилка
-
+
Немає даних
From a342961af29279255750d1893b34cdfb06bfa4c5 Mon Sep 17 00:00:00 2001
From: Roman Trashutin
Date: Tue, 1 Sep 2026 17:19:58 +0300
Subject: [PATCH 05/10] feat(dashboard): totalEvents, events delta, failures
feed, table sizes, spike badge
---
app/pages/dash.vue | 148 ++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 141 insertions(+), 7 deletions(-)
diff --git a/app/pages/dash.vue b/app/pages/dash.vue
index 8d85b88..637806a 100644
--- a/app/pages/dash.vue
+++ b/app/pages/dash.vue
@@ -29,6 +29,7 @@ interface SyncRun {
totalGroups: number
failedGroups: number
removedEvents: number
+ totalEvents: number
steps: SyncSteps
}
@@ -37,10 +38,25 @@ interface SyncRunGroup {
groupId: number
status: "success" | "failed"
eventsCount: number
+ prevEventsCount: number | null
error: string | null
finishedAt: string
}
+interface FailedGroupEntry {
+ runId: number
+ groupId: number
+ error: string | null
+ finishedAt: string
+}
+
+interface TableSizeEntry {
+ tableName: string
+ rowCount: number
+ sizePretty: string
+ sizeBytes: number
+}
+
interface Summary {
currentStatus: "running" | "success" | "partial" | "failed" | "unknown"
isRunning: boolean
@@ -61,9 +77,13 @@ interface ApiResponse {
const POLL_RUNNING = 5_000
const POLL_IDLE = 60_000
+// Spike: flag removedEvents when it exceeds this fraction of total events
+const REMOVED_SPIKE_RATIO = 0.05
const summary = ref(null)
const runs = ref([])
+const failures = ref([])
+const tableSizes = ref([])
const selectedRunId = ref(null)
const selectedRunGroups = ref([])
const loadingGroups = ref(false)
@@ -100,9 +120,26 @@ async function fetchGroups(runId: number) {
}
}
+async function fetchFailures() {
+ try {
+ const res = await $fetch>("/dash-api/failures?limit=50")
+ failures.value = res.data ?? []
+ } catch (e) {
+ error.value = e instanceof Error ? e.message : String(e)
+ }
+}
+
+async function fetchTableSizes() {
+ try {
+ const res = await $fetch>("/dash-api/table-sizes")
+ tableSizes.value = res.data ?? []
+ } catch (e) {
+ error.value = e instanceof Error ? e.message : String(e)
+ }
+}
+
async function refresh() {
- await Promise.all([fetchSummary(), fetchRuns()])
- // refresh groups panel if a run is selected
+ await Promise.all([fetchSummary(), fetchRuns(), fetchFailures(), fetchTableSizes()])
if (selectedRunId.value !== null) {
await fetchGroups(selectedRunId.value)
}
@@ -171,6 +208,25 @@ function duration(start: string, end: string | null): string {
if (m > 0) return `${m}хв ${s % 60}с`
return `${s}с`
}
+
+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)
+}
+
+function deltaClass(curr: number, prev: number | null): string {
+ if (prev === null) return ""
+ const diff = curr - prev
+ if (diff === 0) return ""
+ return diff > 0 ? "text-green-600 dark:text-green-400" : "text-destructive"
+}
+
+function isRemovedSpike(run: SyncRun): boolean {
+ if (run.totalEvents === 0) return false
+ return run.removedEvents / run.totalEvents > REMOVED_SPIKE_RATIO
+}
@@ -242,13 +298,14 @@ function duration(start: string, end: string | null): string {
Початок
Тривалість
Групи (провал)
- Видалено подій
+ Подій всього
+ Видалено
Кроки
- Немає даних
+ Немає даних
- {{ run.removedEvents }}
+ {{ run.totalEvents }}
+
+
+ {{ run.removedEvents }}
+
+ ⚠
+
+
+
Група ID
Статус
Подій
+ Δ
Час
Помилка
-
+
Немає даних
@@ -338,7 +409,13 @@ function duration(start: string, end: string | null): string {
{{ g.status }}
- {{ g.eventsCount }}
+ {{ g.eventsCount }}
+
+ {{ deltaLabel(g.eventsCount, g.prevEventsCount) || "—" }}
+
{{ fmt(g.finishedAt) }}
{{ g.error ?? "—" }}
@@ -347,5 +424,62 @@ function duration(start: string, end: string | null): string {
+
+
+
+
+
+ Останні помилки груп
+
+ (останні 50)
+
+
+
+
+
+
+ Запуск ID
+ Група ID
+ Час
+ Помилка
+
+
+
+
+ {{ f.runId }}
+ {{ f.groupId }}
+ {{ fmt(f.finishedAt) }}
+
+ {{ f.error ?? "—" }}
+
+
+
+
+
+
+
+
+
+
Розмір таблиць БД
+
+
+
+
+ Таблиця
+ Рядків
+ Розмір
+
+
+
+
+ {{ t.tableName }}
+
+ {{ t.rowCount.toLocaleString("uk-UA") }}
+
+ {{ t.sizePretty }}
+
+
+
+
From 785868962f8d27dd1203ca1740c7e9da251dcc66 Mon Sep 17 00:00:00 2001
From: Roman Trashutin
Date: Tue, 1 Sep 2026 17:40:01 +0300
Subject: [PATCH 06/10] feat(dashboard): add Basic Auth middleware and inject
x-dash-key header
---
.env.example | 8 +++++
nuxt.config.ts | 7 +++++
server/middleware/dash-auth.ts | 54 +++++++++++++++++++++++++++++++++
server/routes/dash-api/[...].ts | 5 ++-
4 files changed, 73 insertions(+), 1 deletion(-)
create mode 100644 server/middleware/dash-auth.ts
diff --git a/.env.example b/.env.example
index a16ad8c..f5d6216 100644
--- a/.env.example
+++ b/.env.example
@@ -18,3 +18,11 @@ NUXT_PUBLIC_GA_MEASUREMENT_ID=
# In production, point this to the internal API address (e.g. http://schedule-api:8080).
# Default: http://localhost:8080
NUXT_DASH_API_BASE_URL=
+
+# Shared secret sent as x-dash-key to the backend. Must match DASH_API_KEY in schedule-api.
+# Generate with: openssl rand -hex 32
+NUXT_DASH_API_KEY=change_me_to_a_strong_random_secret
+
+# HTTP Basic Auth credentials for the /dash page (browser prompt).
+NUXT_DASH_USER=admin
+NUXT_DASH_PASSWORD=change_me
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 19146e4..637f663 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -38,6 +38,13 @@ export default defineNuxtConfig({
// Backend URL for the dashboard proxy — server-side only, never exposed to client.
// Override via NUXT_DASH_API_BASE_URL env var.
dashApiBaseUrl: "http://localhost:8080",
+ // Shared secret forwarded to the backend as x-dash-key.
+ // Override via NUXT_DASH_API_KEY env var.
+ dashApiKey: "change_me_to_a_strong_random_secret",
+ // HTTP Basic Auth for the /dash page — browser prompt.
+ // Override via NUXT_DASH_USER / NUXT_DASH_PASSWORD env vars.
+ dashUser: "admin",
+ dashPassword: "change_me",
public: {
maintenance: false,
// Bumped on every build — used to bust the IndexedDB query cache on deploy
diff --git a/server/middleware/dash-auth.ts b/server/middleware/dash-auth.ts
new file mode 100644
index 0000000..0f0c16c
--- /dev/null
+++ b/server/middleware/dash-auth.ts
@@ -0,0 +1,54 @@
+import { createHash, timingSafeEqual } from "node:crypto"
+
+const hash = (s: string) => createHash("sha256").update(s).digest()
+
+/**
+ * HTTP Basic Auth for /dash and /dash-api/* routes.
+ *
+ * Browser sends credentials as "Basic base64(user:pass)".
+ * We hash both sides before comparing so the check is timing-safe.
+ *
+ * Fails closed: if NUXT_DASH_USER or NUXT_DASH_PASSWORD is not set,
+ * returns 503 on /dash* instead of serving an unprotected dashboard.
+ */
+export default defineEventHandler((event) => {
+ if (!event.path.startsWith("/dash")) return
+
+ const config = useRuntimeConfig()
+ const expectedUser = config.dashUser as string | undefined
+ const expectedPass = config.dashPassword as string | undefined
+
+ if (!expectedUser || !expectedPass) {
+ setResponseStatus(event, 503)
+ return "Dashboard auth not configured"
+ }
+
+ const authHeader = getRequestHeader(event, "authorization") ?? ""
+ const [scheme, encoded] = authHeader.split(" ")
+
+ if (scheme?.toLowerCase() !== "basic" || !encoded) {
+ setResponseStatus(event, 401)
+ setResponseHeader(event, "WWW-Authenticate", 'Basic realm="Schedule Dashboard"')
+ return "Unauthorized"
+ }
+
+ const decoded = Buffer.from(encoded, "base64").toString("utf8")
+ const colonIdx = decoded.indexOf(":")
+ if (colonIdx === -1) {
+ setResponseStatus(event, 401)
+ setResponseHeader(event, "WWW-Authenticate", 'Basic realm="Schedule Dashboard"')
+ return "Unauthorized"
+ }
+
+ const providedUser = decoded.slice(0, colonIdx)
+ const providedPass = decoded.slice(colonIdx + 1)
+
+ const userOk = timingSafeEqual(hash(providedUser), hash(expectedUser))
+ const passOk = timingSafeEqual(hash(providedPass), hash(expectedPass))
+
+ if (!userOk || !passOk) {
+ setResponseStatus(event, 401)
+ setResponseHeader(event, "WWW-Authenticate", 'Basic realm="Schedule Dashboard"')
+ return "Unauthorized"
+ }
+})
diff --git a/server/routes/dash-api/[...].ts b/server/routes/dash-api/[...].ts
index 17b537f..40f0a7f 100644
--- a/server/routes/dash-api/[...].ts
+++ b/server/routes/dash-api/[...].ts
@@ -12,10 +12,13 @@
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const base = (config.dashApiBaseUrl as string | undefined) || "http://localhost:8080"
+ const apiKey = (config.dashApiKey as string | undefined) || ""
// Strip /dash-api prefix, keep the rest (e.g. /summary, /runs, /runs/123/groups)
const path = event.path.replace(/^\/dash-api/, "") || "/"
const target = `${base.replace(/\/$/, "")}/api/dash${path}`
- return proxyRequest(event, target)
+ return proxyRequest(event, target, {
+ headers: { "x-dash-key": apiKey },
+ })
})
From ae1fccb25c08fa10d9c3a2c503c168af6263c82a Mon Sep 17 00:00:00 2001
From: Roman Trashutin
Date: Tue, 1 Sep 2026 17:52:11 +0300
Subject: [PATCH 07/10] fix(dashboard): full-width tables, step tooltips,
responsive cards
---
app/layouts/dash.vue | 9 +++++
app/pages/dash.vue | 86 ++++++++++++++++++++++++++------------------
2 files changed, 60 insertions(+), 35 deletions(-)
create mode 100644 app/layouts/dash.vue
diff --git a/app/layouts/dash.vue b/app/layouts/dash.vue
new file mode 100644
index 0000000..312873a
--- /dev/null
+++ b/app/layouts/dash.vue
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/pages/dash.vue b/app/pages/dash.vue
index 637806a..ec093de 100644
--- a/app/pages/dash.vue
+++ b/app/pages/dash.vue
@@ -1,6 +1,6 @@
-
+
Schedule Dashboard
@@ -253,7 +265,7 @@ function isRemovedSpike(run: SyncRun): boolean {
-
+
Статус
@@ -289,19 +301,19 @@ function isRemovedSpike(run: SyncRun): boolean {
Останні запуски
-
+
- ID
- Статус
- Тригер
- Початок
- Тривалість
- Групи (провал)
- Подій всього
- Видалено
- Кроки
-
+ ID
+ Статус
+ Тригер
+ Початок
+ Тривалість
+ Групи (провал)
+ Подій всього
+ Видалено
+ Кроки
+
@@ -311,7 +323,9 @@ function isRemovedSpike(run: SyncRun): boolean {
:key="run.id"
:class="selectedRunId === run.id ? 'bg-muted/50' : ''"
>
- {{ run.id }}
+
+ {{ run.id }}
+
{{ run.status }}
@@ -346,14 +360,16 @@ function isRemovedSpike(run: SyncRun): boolean {
- {{ name[0].toUpperCase() }}:{{ step.count }}{{ step.ok ? "" : "✗" }}
+ {{ String(name)[0].toUpperCase() }}:{{ step.count
+ }}{{ step.ok ? "" : "✗" }}
@@ -384,15 +400,15 @@ function isRemovedSpike(run: SyncRun): boolean {
Завантаження…
-
+
- Група ID
- Статус
- Подій
- Δ
- Час
- Помилка
+ Група ID
+ Статус
+ Подій
+ Δ
+ Час
+ Помилка
@@ -417,7 +433,7 @@ function isRemovedSpike(run: SyncRun): boolean {
{{ deltaLabel(g.eventsCount, g.prevEventsCount) || "—" }}
{{ fmt(g.finishedAt) }}
-
+
{{ g.error ?? "—" }}
@@ -435,13 +451,13 @@ function isRemovedSpike(run: SyncRun): boolean {
-
+
- Запуск ID
- Група ID
- Час
- Помилка
+ Запуск ID
+ Група ID
+ Час
+ Помилка
@@ -449,7 +465,7 @@ function isRemovedSpike(run: SyncRun): boolean {
{{ f.runId }}
{{ f.groupId }}
{{ fmt(f.finishedAt) }}
-
+
{{ f.error ?? "—" }}
@@ -462,12 +478,12 @@ function isRemovedSpike(run: SyncRun): boolean {
Розмір таблиць БД
-
+
- Таблиця
- Рядків
- Розмір
+ Таблиця
+ Рядків
+ Розмір
From 39a495fa52c41536f9f86e2f1b223a89e39702e3 Mon Sep 17 00:00:00 2001
From: Roman Trashutin
Date: Tue, 1 Sep 2026 20:31:29 +0300
Subject: [PATCH 08/10] fix(dashboard): rename UiTableTable to UiTable to fix
table rendering
---
app/pages/dash.vue | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/app/pages/dash.vue b/app/pages/dash.vue
index ec093de..671a6df 100644
--- a/app/pages/dash.vue
+++ b/app/pages/dash.vue
@@ -301,7 +301,7 @@ function stepTitle(name: string, step: StepResult): string {
Останні запуски
-
+
ID
@@ -383,7 +383,7 @@ function stepTitle(name: string, step: StepResult): string {
-
+
@@ -400,7 +400,7 @@ function stepTitle(name: string, step: StepResult): string {
Завантаження…
-
+
Група ID
@@ -438,7 +438,7 @@ function stepTitle(name: string, step: StepResult): string {
-
+
@@ -451,7 +451,7 @@ function stepTitle(name: string, step: StepResult): string {
-
+
Запуск ID
@@ -470,7 +470,7 @@ function stepTitle(name: string, step: StepResult): string {
-
+
@@ -478,7 +478,7 @@ function stepTitle(name: string, step: StepResult): string {
Розмір таблиць БД
-
+
Таблиця
@@ -495,7 +495,7 @@ function stepTitle(name: string, step: StepResult): string {
{{ t.sizePretty }}
-
+
From a657f2c01eb57127c53802f6f1f17bf18bd8be47 Mon Sep 17 00:00:00 2001
From: Roman Trashutin
Date: Tue, 1 Sep 2026 21:02:54 +0300
Subject: [PATCH 09/10] fix(dashboard): hide low-value columns on mobile for
readable table
---
app/pages/dash.vue | 34 ++++++++++++++++++++--------------
1 file changed, 20 insertions(+), 14 deletions(-)
diff --git a/app/pages/dash.vue b/app/pages/dash.vue
index 671a6df..3b6be4c 100644
--- a/app/pages/dash.vue
+++ b/app/pages/dash.vue
@@ -304,16 +304,18 @@ function stepTitle(name: string, step: StepResult): string {
- ID
- Статус
- Тригер
- Початок
- Тривалість
- Групи (провал)
- Подій всього
- Видалено
- Кроки
-
+ ID
+ Статус
+ Тригер
+ Початок
+ Тривалість
+ Групи (провал)
+ Подій всього
+ Видалено
+ Кроки
+
@@ -331,7 +333,9 @@ function stepTitle(name: string, step: StepResult): string {
{{ run.status }}
- {{ run.trigger }}
+ {{
+ run.trigger
+ }}
{{ fmt(run.startedAt) }}
{{ duration(run.startedAt, run.finishedAt) }}
@@ -342,8 +346,10 @@ function stepTitle(name: string, step: StepResult): string {
({{ run.failedGroups }} ✗)
- {{ run.totalEvents }}
-
+ {{
+ run.totalEvents
+ }}
+
@@ -356,7 +362,7 @@ function stepTitle(name: string, step: StepResult): string {
-
+
Date: Tue, 1 Sep 2026 21:53:36 +0300
Subject: [PATCH 10/10] fix(dashboard): drop table-fixed, restore hidden cols,
auto-layout tables
---
app/pages/dash.vue | 72 +++++++++++++++++++++-------------------------
1 file changed, 33 insertions(+), 39 deletions(-)
diff --git a/app/pages/dash.vue b/app/pages/dash.vue
index 3b6be4c..ab5e326 100644
--- a/app/pages/dash.vue
+++ b/app/pages/dash.vue
@@ -301,21 +301,19 @@ function stepTitle(name: string, step: StepResult): string {
Останні запуски
-
+
- ID
- Статус
- Тригер
- Початок
- Тривалість
- Групи (провал)
- Подій всього
- Видалено
- Кроки
-
+ ID
+ Статус
+ Тригер
+ Початок
+ Тривалість
+ Групи (провал)
+ Подій всього
+ Видалено
+ Кроки
+
@@ -333,9 +331,7 @@ function stepTitle(name: string, step: StepResult): string {
{{ run.status }}
- {{
- run.trigger
- }}
+ {{ run.trigger }}
{{ fmt(run.startedAt) }}
{{ duration(run.startedAt, run.finishedAt) }}
@@ -346,10 +342,8 @@ function stepTitle(name: string, step: StepResult): string {
({{ run.failedGroups }} ✗)
- {{
- run.totalEvents
- }}
-
+ {{ run.totalEvents }}
+
@@ -362,7 +356,7 @@ function stepTitle(name: string, step: StepResult): string {
-
+
Завантаження…
-
+
- Група ID
- Статус
- Подій
- Δ
- Час
- Помилка
+ Група ID
+ Статус
+ Подій
+ Δ
+ Час
+ Помилка
@@ -439,7 +433,7 @@ function stepTitle(name: string, step: StepResult): string {
{{ deltaLabel(g.eventsCount, g.prevEventsCount) || "—" }}
{{ fmt(g.finishedAt) }}
-
+
{{ g.error ?? "—" }}
@@ -457,13 +451,13 @@ function stepTitle(name: string, step: StepResult): string {
-
+
- Запуск ID
- Група ID
- Час
- Помилка
+ Запуск ID
+ Група ID
+ Час
+ Помилка
@@ -471,7 +465,7 @@ function stepTitle(name: string, step: StepResult): string {
{{ f.runId }}
{{ f.groupId }}
{{ fmt(f.finishedAt) }}
-
+
{{ f.error ?? "—" }}
@@ -484,12 +478,12 @@ function stepTitle(name: string, step: StepResult): string {
Розмір таблиць БД
-
+
- Таблиця
- Рядків
- Розмір
+ Таблиця
+ Рядків
+ Розмір