db(status-dashboard): 🗃️ Introduce initial status dashboard database schema with tables and aggregate functions

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-28 15:52:23 -08:00
parent 10ec03bace
commit b5df508eb2
6 changed files with 304 additions and 311 deletions

View file

@ -6,7 +6,7 @@ import {
DockerEvent,
ContainerDependency,
} from './entities';
import { InitialSchema1735200000000 } from './migrations/1735200000000-InitialSchema';
import { InitialSchema1700000000000 } from './migrations/1700000000000-InitialSchema';
/**
* Standalone DataSource for TypeORM CLI operations (migrations).
@ -16,7 +16,7 @@ export const AppDataSource = new DataSource({
type: 'better-sqlite3',
database: process.env.DATABASE_SQLITE_PATH ?? './status-dashboard.db',
entities: [VpsResourceSnapshot, DockerContainerSnapshot, DockerEvent, ContainerDependency],
migrations: [InitialSchema1735200000000],
migrations: [InitialSchema1700000000000],
migrationsTableName: 'migrations',
synchronize: false,
logging: process.env.NODE_ENV !== 'production' ? ['error', 'warn'] : false,

View file

@ -13,8 +13,7 @@ import {
ContainerSnapshotHourly,
ContainerSnapshotDaily,
} from './entities';
import { InitialSchema1735200000000 } from './migrations/1735200000000-InitialSchema';
import { AddAggregateTables1735300000000 } from './migrations/1735300000000-AddAggregateTables';
import { InitialSchema1700000000000 } from './migrations/1700000000000-InitialSchema';
export const getDatabaseConfig = (
configService: ConfigService,
@ -32,7 +31,7 @@ export const getDatabaseConfig = (
ContainerSnapshotHourly,
ContainerSnapshotDaily,
],
migrations: [InitialSchema1735200000000, AddAggregateTables1735300000000],
migrations: [InitialSchema1700000000000],
migrationsTableName: 'migrations',
// Enable synchronization in development only
synchronize: configService.isDevelopment,

View file

@ -0,0 +1,299 @@
import { Table, TableIndex } from 'typeorm';
import type { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Initial Status Dashboard Schema (SQLite / better-sqlite3)
*
* Creates all tables for the status-dashboard feature:
* - vps_resource_snapshots: Raw VPS resource metrics (retained 48h)
* - docker_container_snapshots: Raw container metrics (retained 48h)
* - docker_events: Docker lifecycle events
* - container_dependencies: Inter-container dependency graph
* - vps_resource_hourly: Hourly VPS aggregates (retained 6 weeks)
* - vps_resource_daily: Daily VPS aggregates (retained 380 days)
* - container_snapshot_hourly: Hourly container aggregates (retained 6 weeks)
* - container_snapshot_daily: Daily container aggregates (retained 380 days)
*/
export class InitialSchema1700000000000 implements MigrationInterface {
name = 'InitialSchema1700000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// ── vps_resource_snapshots ────────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'vps_resource_snapshots',
columns: [
{
name: 'id',
type: 'varchar',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'cpuPercent', type: 'real' },
{ name: 'cpuCores', type: 'integer' },
{ name: 'memoryUsedMB', type: 'real' },
{ name: 'memoryTotalMB', type: 'real' },
{ name: 'memoryPercent', type: 'real' },
{ name: 'diskUsedGB', type: 'real' },
{ name: 'diskTotalGB', type: 'real' },
{ name: 'diskPercent', type: 'real' },
{ name: 'networkRxBytes', type: 'bigint', default: '0' },
{ name: 'networkTxBytes', type: 'bigint', default: '0' },
{ name: 'timestamp', type: 'datetime' },
],
}),
true,
);
await queryRunner.createIndex(
'vps_resource_snapshots',
new TableIndex({
name: 'idx_vps_snapshot_timestamp',
columnNames: ['timestamp'],
}),
);
// ── docker_container_snapshots ────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'docker_container_snapshots',
columns: [
{
name: 'id',
type: 'varchar',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'containerName', type: 'varchar', length: '255' },
{ name: 'state', type: 'varchar', length: '50' },
{ name: 'health', type: 'varchar', length: '50', isNullable: true },
{ name: 'status', type: 'varchar', length: '255' },
{ name: 'cpuPercent', type: 'real' },
{ name: 'memoryUsage', type: 'varchar', length: '100' },
{ name: 'uptimeSeconds', type: 'integer', isNullable: true },
{ name: 'restartCount', type: 'integer', default: '0' },
{ name: 'timestamp', type: 'datetime' },
],
}),
true,
);
await queryRunner.createIndex(
'docker_container_snapshots',
new TableIndex({
name: 'idx_container_snapshot_name',
columnNames: ['containerName'],
}),
);
await queryRunner.createIndex(
'docker_container_snapshots',
new TableIndex({
name: 'idx_container_snapshot_timestamp',
columnNames: ['timestamp'],
}),
);
// ── docker_events ─────────────────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'docker_events',
columns: [
{
name: 'id',
type: 'varchar',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'containerName', type: 'varchar', length: '255' },
{ name: 'type', type: 'varchar', length: '100' },
{ name: 'action', type: 'varchar', length: '100' },
{ name: 'timestamp', type: 'datetime' },
{ name: 'metadata', type: 'text', isNullable: true },
],
}),
true,
);
await queryRunner.createIndex(
'docker_events',
new TableIndex({
name: 'idx_docker_event_container_name',
columnNames: ['containerName'],
}),
);
await queryRunner.createIndex(
'docker_events',
new TableIndex({
name: 'idx_docker_event_timestamp',
columnNames: ['timestamp'],
}),
);
// ── container_dependencies ────────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'container_dependencies',
columns: [
{ name: 'fromContainer', type: 'varchar', length: '255', isPrimary: true },
{ name: 'toContainer', type: 'varchar', length: '255', isPrimary: true },
{ name: 'updatedAt', type: 'datetime' },
],
}),
true,
);
// ── vps_resource_hourly ───────────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'vps_resource_hourly',
columns: [
{ name: 'id', type: 'varchar', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'hour', type: 'datetime' },
{ name: 'cpuPercentAvg', type: 'real' },
{ name: 'cpuPercentMin', type: 'real' },
{ name: 'cpuPercentMax', type: 'real' },
{ name: 'memoryPercentAvg', type: 'real' },
{ name: 'memoryPercentMin', type: 'real' },
{ name: 'memoryPercentMax', type: 'real' },
{ name: 'memoryUsedMBAvg', type: 'real' },
{ name: 'diskPercentAvg', type: 'real' },
{ name: 'diskUsedGBAvg', type: 'real' },
{ name: 'networkRxBytesTotal', type: 'bigint', default: '0' },
{ name: 'networkTxBytesTotal', type: 'bigint', default: '0' },
{ name: 'sampleCount', type: 'integer' },
],
}),
true,
);
await queryRunner.createIndex(
'vps_resource_hourly',
new TableIndex({ name: 'idx_vps_hourly_host', columnNames: ['vpsHost'] }),
);
await queryRunner.createIndex(
'vps_resource_hourly',
new TableIndex({ name: 'idx_vps_hourly_hour', columnNames: ['hour'] }),
);
// ── vps_resource_daily ────────────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'vps_resource_daily',
columns: [
{ name: 'id', type: 'varchar', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'date', type: 'date' },
{ name: 'cpuPercentAvg', type: 'real' },
{ name: 'cpuPercentMin', type: 'real' },
{ name: 'cpuPercentMax', type: 'real' },
{ name: 'cpuPercentP95', type: 'real' },
{ name: 'memoryPercentAvg', type: 'real' },
{ name: 'memoryPercentMin', type: 'real' },
{ name: 'memoryPercentMax', type: 'real' },
{ name: 'memoryPercentP95', type: 'real' },
{ name: 'memoryUsedMBAvg', type: 'real' },
{ name: 'diskPercentAvg', type: 'real' },
{ name: 'diskUsedGBAvg', type: 'real' },
{ name: 'networkRxBytesTotal', type: 'bigint', default: '0' },
{ name: 'networkTxBytesTotal', type: 'bigint', default: '0' },
{ name: 'sampleCount', type: 'integer' },
],
}),
true,
);
await queryRunner.createIndex(
'vps_resource_daily',
new TableIndex({ name: 'idx_vps_daily_host', columnNames: ['vpsHost'] }),
);
await queryRunner.createIndex(
'vps_resource_daily',
new TableIndex({ name: 'idx_vps_daily_date', columnNames: ['date'] }),
);
// ── container_snapshot_hourly ─────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'container_snapshot_hourly',
columns: [
{ name: 'id', type: 'varchar', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'containerName', type: 'varchar', length: '255' },
{ name: 'hour', type: 'datetime' },
{ name: 'predominantState', type: 'varchar', length: '50' },
{ name: 'healthyPercent', type: 'real' },
{ name: 'cpuPercentAvg', type: 'real' },
{ name: 'cpuPercentMax', type: 'real' },
{ name: 'restartCountMax', type: 'integer', default: '0' },
{ name: 'uptimeSecondsEnd', type: 'integer', isNullable: true },
{ name: 'sampleCount', type: 'integer' },
],
}),
true,
);
await queryRunner.createIndex(
'container_snapshot_hourly',
new TableIndex({ name: 'idx_container_hourly_name', columnNames: ['containerName'] }),
);
await queryRunner.createIndex(
'container_snapshot_hourly',
new TableIndex({ name: 'idx_container_hourly_hour', columnNames: ['hour'] }),
);
// ── container_snapshot_daily ──────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'container_snapshot_daily',
columns: [
{ name: 'id', type: 'varchar', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'containerName', type: 'varchar', length: '255' },
{ name: 'date', type: 'date' },
{ name: 'uptimePercent', type: 'real' },
{ name: 'healthyPercent', type: 'real' },
{ name: 'cpuPercentAvg', type: 'real' },
{ name: 'cpuPercentMax', type: 'real' },
{ name: 'cpuPercentP95', type: 'real' },
{ name: 'restartCount', type: 'integer', default: '0' },
{ name: 'sampleCount', type: 'integer' },
],
}),
true,
);
await queryRunner.createIndex(
'container_snapshot_daily',
new TableIndex({ name: 'idx_container_daily_name', columnNames: ['containerName'] }),
);
await queryRunner.createIndex(
'container_snapshot_daily',
new TableIndex({ name: 'idx_container_daily_date', columnNames: ['date'] }),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('container_snapshot_daily');
await queryRunner.dropTable('container_snapshot_hourly');
await queryRunner.dropTable('vps_resource_daily');
await queryRunner.dropTable('vps_resource_hourly');
await queryRunner.dropTable('container_dependencies');
await queryRunner.dropTable('docker_events');
await queryRunner.dropTable('docker_container_snapshots');
await queryRunner.dropTable('vps_resource_snapshots');
}
}

View file

@ -1,148 +0,0 @@
import { Table, TableIndex } from 'typeorm';
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class InitialSchema1735200000000 implements MigrationInterface {
name = 'InitialSchema1735200000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// VPS Resource Snapshots
await queryRunner.createTable(
new Table({
name: 'vps_resource_snapshots',
columns: [
{
name: 'id',
type: 'varchar',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'cpuPercent', type: 'real' },
{ name: 'cpuCores', type: 'integer' },
{ name: 'memoryUsedMB', type: 'real' },
{ name: 'memoryTotalMB', type: 'real' },
{ name: 'memoryPercent', type: 'real' },
{ name: 'diskUsedGB', type: 'real' },
{ name: 'diskTotalGB', type: 'real' },
{ name: 'diskPercent', type: 'real' },
{ name: 'networkRxBytes', type: 'bigint', default: '0' },
{ name: 'networkTxBytes', type: 'bigint', default: '0' },
{ name: 'timestamp', type: 'datetime' },
],
}),
true,
);
await queryRunner.createIndex(
'vps_resource_snapshots',
new TableIndex({
name: 'idx_vps_snapshot_timestamp',
columnNames: ['timestamp'],
}),
);
// Docker Container Snapshots
await queryRunner.createTable(
new Table({
name: 'docker_container_snapshots',
columns: [
{
name: 'id',
type: 'varchar',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'containerName', type: 'varchar', length: '255' },
{ name: 'state', type: 'varchar', length: '50' },
{ name: 'health', type: 'varchar', length: '50', isNullable: true },
{ name: 'status', type: 'varchar', length: '255' },
{ name: 'cpuPercent', type: 'real' },
{ name: 'memoryUsage', type: 'varchar', length: '100' },
{ name: 'uptimeSeconds', type: 'integer', isNullable: true },
{ name: 'restartCount', type: 'integer', default: '0' },
{ name: 'timestamp', type: 'datetime' },
],
}),
true,
);
await queryRunner.createIndex(
'docker_container_snapshots',
new TableIndex({
name: 'idx_container_snapshot_name',
columnNames: ['containerName'],
}),
);
await queryRunner.createIndex(
'docker_container_snapshots',
new TableIndex({
name: 'idx_container_snapshot_timestamp',
columnNames: ['timestamp'],
}),
);
// Docker Events
await queryRunner.createTable(
new Table({
name: 'docker_events',
columns: [
{
name: 'id',
type: 'varchar',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'containerName', type: 'varchar', length: '255' },
{ name: 'type', type: 'varchar', length: '100' },
{ name: 'action', type: 'varchar', length: '100' },
{ name: 'timestamp', type: 'datetime' },
{ name: 'metadata', type: 'text', isNullable: true },
],
}),
true,
);
await queryRunner.createIndex(
'docker_events',
new TableIndex({
name: 'idx_docker_event_container_name',
columnNames: ['containerName'],
}),
);
await queryRunner.createIndex(
'docker_events',
new TableIndex({
name: 'idx_docker_event_timestamp',
columnNames: ['timestamp'],
}),
);
// Container Dependencies
await queryRunner.createTable(
new Table({
name: 'container_dependencies',
columns: [
{ name: 'fromContainer', type: 'varchar', length: '255', isPrimary: true },
{ name: 'toContainer', type: 'varchar', length: '255', isPrimary: true },
{ name: 'updatedAt', type: 'datetime' },
],
}),
true,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('container_dependencies');
await queryRunner.dropTable('docker_events');
await queryRunner.dropTable('docker_container_snapshots');
await queryRunner.dropTable('vps_resource_snapshots');
}
}

View file

@ -1,158 +0,0 @@
import { Table, TableIndex } from 'typeorm';
import type { MigrationInterface, QueryRunner} from 'typeorm';
/**
* Migration to add aggregate tables for data retention
*
* Retention tiers:
* - Raw snapshots: 48 hours
* - Hourly aggregates: 6 weeks
* - Daily aggregates: 380 days (then archived to bigdisk)
*/
export class AddAggregateTables1735300000000 implements MigrationInterface {
name = 'AddAggregateTables1735300000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// VPS Resource Hourly Aggregates
await queryRunner.createTable(
new Table({
name: 'vps_resource_hourly',
columns: [
{ name: 'id', type: 'varchar', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'hour', type: 'datetime' },
{ name: 'cpuPercentAvg', type: 'real' },
{ name: 'cpuPercentMin', type: 'real' },
{ name: 'cpuPercentMax', type: 'real' },
{ name: 'memoryPercentAvg', type: 'real' },
{ name: 'memoryPercentMin', type: 'real' },
{ name: 'memoryPercentMax', type: 'real' },
{ name: 'memoryUsedMBAvg', type: 'real' },
{ name: 'diskPercentAvg', type: 'real' },
{ name: 'diskUsedGBAvg', type: 'real' },
{ name: 'networkRxBytesTotal', type: 'bigint', default: '0' },
{ name: 'networkTxBytesTotal', type: 'bigint', default: '0' },
{ name: 'sampleCount', type: 'integer' },
],
}),
true,
);
await queryRunner.createIndex(
'vps_resource_hourly',
new TableIndex({ name: 'idx_vps_hourly_host', columnNames: ['vpsHost'] }),
);
await queryRunner.createIndex(
'vps_resource_hourly',
new TableIndex({ name: 'idx_vps_hourly_hour', columnNames: ['hour'] }),
);
// VPS Resource Daily Aggregates
await queryRunner.createTable(
new Table({
name: 'vps_resource_daily',
columns: [
{ name: 'id', type: 'varchar', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'date', type: 'date' },
{ name: 'cpuPercentAvg', type: 'real' },
{ name: 'cpuPercentMin', type: 'real' },
{ name: 'cpuPercentMax', type: 'real' },
{ name: 'cpuPercentP95', type: 'real' },
{ name: 'memoryPercentAvg', type: 'real' },
{ name: 'memoryPercentMin', type: 'real' },
{ name: 'memoryPercentMax', type: 'real' },
{ name: 'memoryPercentP95', type: 'real' },
{ name: 'memoryUsedMBAvg', type: 'real' },
{ name: 'diskPercentAvg', type: 'real' },
{ name: 'diskUsedGBAvg', type: 'real' },
{ name: 'networkRxBytesTotal', type: 'bigint', default: '0' },
{ name: 'networkTxBytesTotal', type: 'bigint', default: '0' },
{ name: 'sampleCount', type: 'integer' },
],
}),
true,
);
await queryRunner.createIndex(
'vps_resource_daily',
new TableIndex({ name: 'idx_vps_daily_host', columnNames: ['vpsHost'] }),
);
await queryRunner.createIndex(
'vps_resource_daily',
new TableIndex({ name: 'idx_vps_daily_date', columnNames: ['date'] }),
);
// Container Snapshot Hourly Aggregates
await queryRunner.createTable(
new Table({
name: 'container_snapshot_hourly',
columns: [
{ name: 'id', type: 'varchar', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'containerName', type: 'varchar', length: '255' },
{ name: 'hour', type: 'datetime' },
{ name: 'predominantState', type: 'varchar', length: '50' },
{ name: 'healthyPercent', type: 'real' },
{ name: 'cpuPercentAvg', type: 'real' },
{ name: 'cpuPercentMax', type: 'real' },
{ name: 'restartCountMax', type: 'integer', default: '0' },
{ name: 'uptimeSecondsEnd', type: 'integer', isNullable: true },
{ name: 'sampleCount', type: 'integer' },
],
}),
true,
);
await queryRunner.createIndex(
'container_snapshot_hourly',
new TableIndex({ name: 'idx_container_hourly_name', columnNames: ['containerName'] }),
);
await queryRunner.createIndex(
'container_snapshot_hourly',
new TableIndex({ name: 'idx_container_hourly_hour', columnNames: ['hour'] }),
);
// Container Snapshot Daily Aggregates
await queryRunner.createTable(
new Table({
name: 'container_snapshot_daily',
columns: [
{ name: 'id', type: 'varchar', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'vpsHost', type: 'varchar', length: '255' },
{ name: 'containerName', type: 'varchar', length: '255' },
{ name: 'date', type: 'date' },
{ name: 'uptimePercent', type: 'real' },
{ name: 'healthyPercent', type: 'real' },
{ name: 'cpuPercentAvg', type: 'real' },
{ name: 'cpuPercentMax', type: 'real' },
{ name: 'cpuPercentP95', type: 'real' },
{ name: 'restartCount', type: 'integer', default: '0' },
{ name: 'sampleCount', type: 'integer' },
],
}),
true,
);
await queryRunner.createIndex(
'container_snapshot_daily',
new TableIndex({ name: 'idx_container_daily_name', columnNames: ['containerName'] }),
);
await queryRunner.createIndex(
'container_snapshot_daily',
new TableIndex({ name: 'idx_container_daily_date', columnNames: ['date'] }),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('container_snapshot_daily');
await queryRunner.dropTable('container_snapshot_hourly');
await queryRunner.dropTable('vps_resource_daily');
await queryRunner.dropTable('vps_resource_hourly');
}
}

View file

@ -0,0 +1 @@
export { InitialSchema1700000000000 } from './1700000000000-InitialSchema';