import { sql } from "drizzle-orm"; import { sqliteTable, text, integer, real, uniqueIndex } from "drizzle-orm/sqlite-core"; // ─── Users & roles ────────────────────────────────────────────────────────── export const userRoles = ["admin", "operator", "viewer"] as const; export type UserRole = (typeof userRoles)[number]; export const users = sqliteTable("users", { id: integer("id").primaryKey({ autoIncrement: true }), oidcSub: text("oidc_sub").notNull().unique(), email: text("email"), name: text("name"), role: text("role").$type().notNull().default("viewer"), createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), lastLoginAt: text("last_login_at"), }); // ─── Audit log ────────────────────────────────────────────────────────────── export const auditLog = sqliteTable("audit_log", { id: integer("id").primaryKey({ autoIncrement: true }), actorUserId: integer("actor_user_id").references(() => users.id, { onDelete: "set null" }), actorLabel: text("actor_label"), // denormalized name/email snapshot, survives user deletion category: text("category").notNull(), // 'secret' | 'ipam' | 'dns' | 'user' | 'integration' | 'task' | ... action: text("action").notNull(), // 'create' | 'update' | 'delete' | 'start' | 'stop' | ... targetType: text("target_type"), targetId: text("target_id"), detail: text("detail"), // JSON-encoded free-form context createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), }); // ─── Diagnostic log — every outbound call to a DNS provider or integration ── export const diagLog = sqliteTable("diag_log", { id: integer("id").primaryKey({ autoIncrement: true }), source: text("source").notNull(), // e.g. 'cloudflare' | 'tailscale' | 'proxmox' | ... operation: text("operation").notNull(), // adapter method name, e.g. 'listZones' | 'listDevices' ok: integer("ok", { mode: "boolean" }).notNull(), latencyMs: integer("latency_ms").notNull(), error: text("error"), createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), }); // ─── Notification queue — pending notifications held during quiet hours ──── export const notificationQueue = sqliteTable("notification_queue", { id: integer("id").primaryKey({ autoIncrement: true }), title: text("title").notNull(), message: text("message").notNull(), createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), }); // ─── Maintenance windows — alerts about a target are silenced until endsAt ── export const maintenanceTargetTypes = ["server", "integration", "dns_provider"] as const; export type MaintenanceTargetType = (typeof maintenanceTargetTypes)[number]; export const maintenanceWindows = sqliteTable("maintenance_windows", { id: integer("id").primaryKey({ autoIncrement: true }), targetType: text("target_type").$type().notNull(), targetId: integer("target_id").notNull(), // polymorphic — no FK; a deleted target's window is simply ignored reason: text("reason"), startedAt: text("started_at").notNull(), // ISO endsAt: text("ends_at").notNull(), // ISO — required: a forgotten open-ended window would silence real problems forever createdBy: text("created_by"), }); // ─── Settings (key/value) ─────────────────────────────────────────────────── export const settings = sqliteTable("settings", { key: text("key").primaryKey(), value: text("value").notNull(), updatedAt: text("updated_at") .notNull() .default(sql`(current_timestamp)`), }); // ─── Secrets expiry tracker (ported from Sloth Manager) ──────────────────── export const secretTypes = ["api_token", "ssl_certificate", "password", "generic"] as const; export type SecretType = (typeof secretTypes)[number]; export const secrets = sqliteTable("secrets", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), type: text("type").$type().notNull().default("generic"), description: text("description"), expiryDate: text("expiry_date").notNull(), // ISO date, e.g. 2026-03-01 warnDays: integer("warn_days").notNull().default(30), notes: text("notes"), // ssl_certificate secrets only: when set, expiryDate is read from the live certificate on this host:port instead of typed in. checkHost: text("check_host"), checkPort: integer("check_port"), lastCheckedAt: text("last_checked_at"), lastCheckError: text("last_check_error"), createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), updatedAt: text("updated_at") .notNull() .default(sql`(current_timestamp)`), }); // ─── IPAM (ported from Sloth Manager) ─────────────────────────────────────── export const ipamEntries = sqliteTable("ipam_entries", { id: integer("id").primaryKey({ autoIncrement: true }), ipAddress: text("ip_address").notNull().unique(), label: text("label"), vendor: text("vendor"), location: text("location"), notes: text("notes"), source: text("source"), // null = entered manually; "tailscale" = auto-synced from a Tailscale integration createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), updatedAt: text("updated_at") .notNull() .default(sql`(current_timestamp)`), }); // ─── Integration credentials (encrypted API tokens) ───────────────────────── export const integrationCredentials = sqliteTable("integration_credentials", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), encryptedSecret: text("encrypted_secret").notNull(), // AES-256-GCM, see src/crypto.ts createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), }); // ─── DNS providers + record cache (ported from Sloth Manager) ────────────── export const dnsProviderTypes = [ "cloudflare", "loopia", "pihole", "azure", "cpanel", "technitium", ] as const; export type DnsProviderType = (typeof dnsProviderTypes)[number]; export const dnsProviders = sqliteTable("dns_providers", { id: integer("id").primaryKey({ autoIncrement: true }), providerType: text("provider_type").$type().notNull(), name: text("name").notNull(), credentialId: integer("credential_id").references(() => integrationCredentials.id, { onDelete: "set null", }), config: text("config"), // JSON: non-secret provider config (base URL, zone list, etc.) enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), }); export const dnsZonesCache = sqliteTable( "dns_zones_cache", { id: integer("id").primaryKey({ autoIncrement: true }), providerId: integer("provider_id") .notNull() .references(() => dnsProviders.id, { onDelete: "cascade" }), zoneId: text("zone_id").notNull(), // provider-specific zone identifier zoneName: text("zone_name").notNull(), syncedAt: text("synced_at"), }, (table) => [uniqueIndex("dns_zones_cache_provider_zone_idx").on(table.providerId, table.zoneId)], ); export const dnsRecordsCache = sqliteTable("dns_records_cache", { id: integer("id").primaryKey({ autoIncrement: true }), providerId: integer("provider_id") .notNull() .references(() => dnsProviders.id, { onDelete: "cascade" }), zoneId: text("zone_id").notNull(), recordId: text("record_id").notNull(), // provider-specific record identifier type: text("type").notNull(), // A, AAAA, CNAME, TXT, MX, ... name: text("name").notNull(), content: text("content").notNull(), ttl: integer("ttl"), priority: integer("priority"), proxied: integer("proxied", { mode: "boolean" }), }); // ─── Servers & scheduled tasks (ported from Schedule Task Manager) ───────── export const proxmoxGuestTypes = ["qemu", "lxc"] as const; export type ProxmoxGuestTypeCol = (typeof proxmoxGuestTypes)[number]; export const servers = sqliteTable("servers", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), hostname: text("hostname"), osType: text("os_type").notNull().default("linux"), description: text("description"), apiTokenHash: text("api_token_hash").notNull(), apiTokenPrefix: text("api_token_prefix").notNull(), createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), lastSeenAt: text("last_seen_at"), // Agent-reported hardware/network snapshot — updated on every agent report. ipAddresses: text("ip_addresses"), // JSON string[] cpuModel: text("cpu_model"), cpuCores: integer("cpu_cores"), cpuLoadPercent: real("cpu_load_percent"), memTotalBytes: integer("mem_total_bytes"), memUsedBytes: integer("mem_used_bytes"), disks: text("disks"), // JSON string: {mount, sizeBytes, usedBytes}[] listeningPorts: text("listening_ports"), // JSON string: {protocol, port, address, process}[] — what the agent sees bound on the host lastPortScan: text("last_port_scan"), // JSON string: summary of the most recent network scan from this app // Optional link to a Proxmox VM/LXC — set by an admin, not the agent. proxmoxIntegrationId: integer("proxmox_integration_id").references(() => integrations.id, { onDelete: "set null", }), proxmoxNode: text("proxmox_node"), proxmoxGuestType: text("proxmox_guest_type").$type(), proxmoxVmid: integer("proxmox_vmid"), // Not every server is a Proxmox guest (bare-metal boxes, other hosts) — an // admin can hide the "Proxmox link" card on this server's detail page // rather than seeing an irrelevant option on every server. Ignored (the // card always shows) once a server IS actually linked, so unlinking stays // reachable. hideProxmoxLink: integer("hide_proxmox_link", { mode: "boolean" }).notNull().default(false), }); export const scheduledTasks = sqliteTable("scheduled_tasks", { id: integer("id").primaryKey({ autoIncrement: true }), serverId: integer("server_id") .notNull() .references(() => servers.id, { onDelete: "cascade" }), scheduleType: text("schedule_type").notNull(), // 'cron' | 'systemd_timer' | 'docker' | 'backup' | 'update' | 'n8n_workflow' | 'manual' origin: text("origin").notNull().default("agent"), // 'agent' | 'manual' — manual rows are never touched by agent sync name: text("name").notNull(), command: text("command"), scheduleExpression: text("schedule_expression"), source: text("source"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), nextRunAt: text("next_run_at"), rawMetadata: text("raw_metadata"), isStale: integer("is_stale", { mode: "boolean" }).notNull().default(false), firstSeenAt: text("first_seen_at") .notNull() .default(sql`(current_timestamp)`), lastSeenAt: text("last_seen_at") .notNull() .default(sql`(current_timestamp)`), }); // ─── Server links — admin-page bookmarks per server (Dockge, Webmin, Cockpit, etc.) ─ export const serverLinks = sqliteTable("server_links", { id: integer("id").primaryKey({ autoIncrement: true }), serverId: integer("server_id") .notNull() .references(() => servers.id, { onDelete: "cascade" }), label: text("label").notNull(), url: text("url").notNull(), createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), }); // ─── Live integrations (Proxmox, Synology, Semaphore, Tailscale, Gitea, Dockhand) ─ export const integrationTypes = [ "proxmox", "synology", "semaphore", "tailscale", "gitea", "dockhand", ] as const; export type IntegrationType = (typeof integrationTypes)[number]; export const integrations = sqliteTable("integrations", { id: integer("id").primaryKey({ autoIncrement: true }), type: text("type").$type().notNull(), name: text("name").notNull(), baseUrl: text("base_url").notNull(), credentialId: integer("credential_id").references(() => integrationCredentials.id, { onDelete: "set null", }), config: text("config"), // JSON: per-type non-secret config (tailnet name, node name, etc.) enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), createdAt: text("created_at") .notNull() .default(sql`(current_timestamp)`), }); // A port on a server that's either been seen open (by a scan or the agent) or that someone wrote a note about. // Rows exist only while they carry information: an open port, or one with a label/comment ("reserved"). export const serverPorts = sqliteTable( "server_ports", { id: integer("id").primaryKey({ autoIncrement: true }), serverId: integer("server_id") .notNull() .references(() => servers.id, { onDelete: "cascade" }), port: integer("port").notNull(), protocol: text("protocol").$type<"tcp" | "udp">().notNull().default("tcp"), label: text("label"), comment: text("comment"), // True when the last network scan connected to it. The agent's view is stored on the server row instead. open: integer("open", { mode: "boolean" }).notNull().default(false), lastSeenOpenAt: text("last_seen_open_at"), updatedAt: text("updated_at") .notNull() .default(sql`(current_timestamp)`), }, (t) => [uniqueIndex("server_ports_unique").on(t.serverId, t.port, t.protocol)], );