netpulse-migrate замість PowerShell-скрипта: у контейнері немає ані psql, ані PowerShell, а тягнути клієнт Postgres в образ заради одного запуску — це половина дистрибутива на порожньому місці. Міграції вшиті через embed і переїхали в server/migrations: embed не бачить нічого за межами кореня свого модуля, а міграції поруч із бінарником, який їх накочує, не можуть розійтися версіями. Накочування під advisory-блокуванням: два інстанси при rolling update інакше застосували б ту саму міграцію двічі. Кожен файл в одній транзакції разом із записом у schema_migrations; виняток — continuous aggregates, які TimescaleDB забороняє в транзакції. Змінена вже застосована міграція зупиняє запуск: у різних інсталяціях інакше опиниться різна схема під одним номером. Перевірено на чистій базі: 23 міграції, 101 таблиця, повторний запуск каже «схема актуальна». Веб віддає сам API через embed: на self-hosted це прибирає з інструкції встановлення цілий компонент. Три політики кешування — назавжди для assets із хешем у імені, ніколи для index.html, коротко для решти. Знайдено живим прогоном: невідомий шлях під /api/ віддавав 200 з index.html, і клієнт падав на розборі HTML як JSON замість чесного 404. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
5 KiB
SQL
102 lines
5 KiB
SQL
-- =====================================================================
|
||
-- NetPulse :: 0008_dashboards.sql
|
||
-- Дашборди (в т.ч. NOC/TV-режим), віджети, збережені фільтри, SLA-звіти.
|
||
-- =====================================================================
|
||
|
||
CREATE TYPE core.dashboard_kind AS ENUM ('standard','noc_tv','mobile','embed');
|
||
|
||
CREATE TABLE core.dashboards (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
name text NOT NULL,
|
||
slug core.slug NOT NULL,
|
||
kind core.dashboard_kind NOT NULL DEFAULT 'standard',
|
||
-- Сітка: {"cols":24,"rowHeight":40,"compact":"vertical"}
|
||
layout jsonb NOT NULL DEFAULT '{"cols":24,"rowHeight":40}'::jsonb,
|
||
-- TV-режим: автопрокрутка сторінок, темна тема, приховані меню
|
||
tv_options jsonb NOT NULL DEFAULT '{"rotate_sec":0,"hide_chrome":true,"theme":"dark"}'::jsonb,
|
||
refresh_sec int NOT NULL DEFAULT 30,
|
||
time_range jsonb NOT NULL DEFAULT '{"from":"now-6h","to":"now"}'::jsonb,
|
||
variables jsonb NOT NULL DEFAULT '[]'::jsonb, -- $site, $group для фільтрів
|
||
is_default boolean NOT NULL DEFAULT false,
|
||
public_token text UNIQUE, -- read-only лінк для телевізора в NOC
|
||
created_by uuid REFERENCES core.users(id) ON DELETE SET NULL,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (tenant_id, slug)
|
||
);
|
||
|
||
CREATE TYPE core.widget_kind AS ENUM (
|
||
'map', -- повноекранна мапа топології
|
||
'timeseries','gauge','stat','table','heatmap','topn','pie',
|
||
'alert_list','device_grid','status_matrix','uptime_bar',
|
||
'syslog_stream','ncm_changes','text','iframe','weather_map'
|
||
);
|
||
|
||
CREATE TABLE core.dashboard_widgets (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
dashboard_id uuid NOT NULL REFERENCES core.dashboards(id) ON DELETE CASCADE,
|
||
kind core.widget_kind NOT NULL,
|
||
title text,
|
||
-- Позиція в сітці
|
||
grid_x int NOT NULL DEFAULT 0,
|
||
grid_y int NOT NULL DEFAULT 0,
|
||
grid_w int NOT NULL DEFAULT 6,
|
||
grid_h int NOT NULL DEFAULT 4,
|
||
-- Для kind='map'
|
||
map_id uuid REFERENCES topo.maps(id) ON DELETE SET NULL,
|
||
-- Джерела даних: [{"metric_key":"cpu.util","selector":{...},"agg":"avg"}]
|
||
queries jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||
options jsonb NOT NULL DEFAULT '{}'::jsonb, -- осі, кольори, пороги, легенда
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now()
|
||
);
|
||
CREATE INDEX widgets_dashboard_idx ON core.dashboard_widgets (dashboard_id);
|
||
|
||
-- Збережені фільтри / представлення інвентарю
|
||
CREATE TABLE core.saved_views (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
user_id uuid REFERENCES core.users(id) ON DELETE CASCADE, -- NULL = спільний
|
||
scope text NOT NULL, -- devices | alerts | maps | ncm
|
||
name text NOT NULL,
|
||
filter jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||
columns jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||
sort jsonb,
|
||
created_at timestamptz NOT NULL DEFAULT now()
|
||
);
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- SLA / uptime-звіти
|
||
-- ---------------------------------------------------------------------
|
||
|
||
CREATE TABLE core.sla_targets (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
name text NOT NULL,
|
||
selector jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||
target_pct numeric(5,3) NOT NULL DEFAULT 99.900,
|
||
-- 'window' — зарезервоване слово в PostgreSQL, тому period_kind
|
||
period_kind text NOT NULL DEFAULT 'monthly', -- daily | weekly | monthly | quarterly
|
||
-- Робочі години, поза якими простій не рахується
|
||
business_hours jsonb,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (tenant_id, name)
|
||
);
|
||
|
||
CREATE TABLE core.sla_periods (
|
||
id uuid PRIMARY KEY DEFAULT core.new_id(),
|
||
tenant_id uuid NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE,
|
||
sla_target_id uuid NOT NULL REFERENCES core.sla_targets(id) ON DELETE CASCADE,
|
||
device_id uuid REFERENCES inv.devices(id) ON DELETE CASCADE,
|
||
period daterange NOT NULL,
|
||
uptime_pct numeric(6,3) NOT NULL,
|
||
downtime_sec bigint NOT NULL DEFAULT 0,
|
||
maintenance_sec bigint NOT NULL DEFAULT 0,
|
||
incidents int NOT NULL DEFAULT 0,
|
||
breached boolean NOT NULL DEFAULT false,
|
||
computed_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (sla_target_id, device_id, period)
|
||
);
|
||
CREATE INDEX sla_periods_tenant_idx ON core.sla_periods (tenant_id, period);
|