From ec5214fd44439edc55aa761cfe00457d4fe37941 Mon Sep 17 00:00:00 2001 From: Cypress Reed Date: Sat, 18 Jul 2026 16:54:14 -0600 Subject: [PATCH 1/8] add handling for managed hosts and ultramarine server branding (real) --- apps/dashboard/.env.example | 5 + .../drizzle/0028_lame_james_howlett.sql | 29 + .../dashboard/drizzle/meta/0028_snapshot.json | 2790 +++++++++++++++++ apps/dashboard/drizzle/meta/_journal.json | 9 +- apps/dashboard/src/app.d.ts | 2 + apps/dashboard/src/lib/branding.ts | 32 + .../dialogs/totp-onboarding-dialog.svelte | 3 +- apps/dashboard/src/lib/feature-flags.ts | 7 +- .../src/lib/remote/feature-flags.remote.ts | 2 +- .../src/lib/remote/managed-hosts.remote.ts | 242 ++ apps/dashboard/src/lib/server/auth.ts | 3 +- apps/dashboard/src/lib/server/db/schema.ts | 69 + apps/dashboard/src/lib/server/tetra/client.ts | 98 + .../dashboard/src/routes/(app)/+layout.svelte | 47 +- apps/dashboard/src/routes/(app)/+page.svelte | 9 +- .../routes/(app)/admin/features/+page.svelte | 1 + .../projects/[projectid]/+page.server.ts | 3 +- .../[projectid]/hosts/+page.server.ts | 20 + .../projects/[projectid]/hosts/+page.svelte | 216 ++ .../[projectid]/hosts/[id]/+page.server.ts | 21 + .../[projectid]/hosts/[id]/+page.svelte | 192 ++ apps/dashboard/src/routes/+layout.svelte | 18 +- .../[invitationId]/+page.svelte | 13 +- apps/dashboard/src/routes/layout.css | 69 + apps/dashboard/src/routes/login/+page.svelte | 13 +- .../login/two-factor/passkey/+page.svelte | 15 +- .../routes/login/two-factor/totp/+page.svelte | 11 +- .../src/routes/register/+page.svelte | 13 +- .../static/ultramarine-apple-touch-icon.png | Bin 0 -> 11364 bytes .../static/ultramarine-favicon-16x16.png | Bin 0 -> 854 bytes .../static/ultramarine-favicon-32x32.png | Bin 0 -> 1134 bytes apps/dashboard/static/ultramarine-favicon.ico | Bin 0 -> 4414 bytes apps/dashboard/static/ultramarine-logo.svg | 1 + 33 files changed, 3899 insertions(+), 54 deletions(-) create mode 100644 apps/dashboard/drizzle/0028_lame_james_howlett.sql create mode 100644 apps/dashboard/drizzle/meta/0028_snapshot.json create mode 100644 apps/dashboard/src/lib/branding.ts create mode 100644 apps/dashboard/src/lib/remote/managed-hosts.remote.ts create mode 100644 apps/dashboard/src/lib/server/tetra/client.ts create mode 100644 apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/+page.server.ts create mode 100644 apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/+page.svelte create mode 100644 apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/[id]/+page.server.ts create mode 100644 apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/[id]/+page.svelte create mode 100644 apps/dashboard/static/ultramarine-apple-touch-icon.png create mode 100644 apps/dashboard/static/ultramarine-favicon-16x16.png create mode 100644 apps/dashboard/static/ultramarine-favicon-32x32.png create mode 100644 apps/dashboard/static/ultramarine-favicon.ico create mode 100644 apps/dashboard/static/ultramarine-logo.svg diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index ab564cf..5d17807 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -5,6 +5,11 @@ CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://postgres:my # The canonical URL. ORIGIN="http://localhost:5173" +# Branding +# Use "ultramarine" for standalone Ultramarine Server deployments. +# This is read at build time. Defaults to "stack" when unset. +PUBLIC_DASHBOARD_BRAND="stack" + # Better Auth # For production use 32+ characters generated with high entropy # https://www.better-auth.com/docs/installation diff --git a/apps/dashboard/drizzle/0028_lame_james_howlett.sql b/apps/dashboard/drizzle/0028_lame_james_howlett.sql new file mode 100644 index 0000000..ec1f336 --- /dev/null +++ b/apps/dashboard/drizzle/0028_lame_james_howlett.sql @@ -0,0 +1,29 @@ +CREATE TYPE "public"."managed_host_connection_mode" AS ENUM('direct_http', 'websocket', 'vsock_gateway', 'offline');--> statement-breakpoint +CREATE TYPE "public"."managed_host_connection_state" AS ENUM('online', 'offline', 'unknown');--> statement-breakpoint +CREATE TYPE "public"."managed_host_kind" AS ENUM('stack_vps', 'external', 'local');--> statement-breakpoint +CREATE TABLE "managed_hosts" ( + "id" text PRIMARY KEY NOT NULL, + "display_name" text NOT NULL, + "owner_project_id" text NOT NULL, + "host_kind" "managed_host_kind" DEFAULT 'external' NOT NULL, + "linked_vm_id" text, + "connection_mode" "managed_host_connection_mode" DEFAULT 'direct_http' NOT NULL, + "connection_state" "managed_host_connection_state" DEFAULT 'unknown' NOT NULL, + "agent_url" text, + "bearer_token" text, + "last_seen_at" bigint, + "agent_version" text, + "hostname" text, + "os" text, + "arch" text, + "capabilities" jsonb, + "last_error" text, + "created_at" bigint DEFAULT (extract(epoch from now()) * 1000)::bigint NOT NULL, + "updated_at" bigint DEFAULT (extract(epoch from now()) * 1000)::bigint NOT NULL +); +--> statement-breakpoint +ALTER TABLE "managed_hosts" ADD CONSTRAINT "managed_hosts_owner_project_id_organization_id_fk" FOREIGN KEY ("owner_project_id") REFERENCES "public"."organization"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "managed_hosts" ADD CONSTRAINT "managed_hosts_linked_vm_id_vms_id_fk" FOREIGN KEY ("linked_vm_id") REFERENCES "public"."vms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "managed_hosts_owner_project_id_index" ON "managed_hosts" USING btree ("owner_project_id");--> statement-breakpoint +CREATE INDEX "managed_hosts_linked_vm_id_index" ON "managed_hosts" USING btree ("linked_vm_id");--> statement-breakpoint +CREATE INDEX "managed_hosts_connection_state_index" ON "managed_hosts" USING btree ("connection_state"); \ No newline at end of file diff --git a/apps/dashboard/drizzle/meta/0028_snapshot.json b/apps/dashboard/drizzle/meta/0028_snapshot.json new file mode 100644 index 0000000..7b4546c --- /dev/null +++ b/apps/dashboard/drizzle/meta/0028_snapshot.json @@ -0,0 +1,2790 @@ +{ + "id": "3ef743b4-a356-4a73-a24a-bcbc0fc7d9ee", + "prevId": "f6ddb885-2f3b-4eeb-8a7f-b5176fe15d74", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_tokens": { + "name": "api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "api_tokens_user_id_index": { + "name": "api_tokens_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.base_images": { + "name": "base_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bg-gray-600'" + }, + "is_official": { + "name": "is_official", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logo_svg": { + "name": "logo_svg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#6b7280'" + }, + "image_type": { + "name": "image_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'qcow2'" + }, + "secure_boot": { + "name": "secure_boot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_meters": { + "name": "billing_meters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "units": { + "name": "units", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_metered_at": { + "name": "last_metered_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_meters_resource_index": { + "name": "billing_meters_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_active_last_metered_at_index": { + "name": "billing_meters_active_last_metered_at_index", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_metered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_project_active_index": { + "name": "billing_meters_project_active_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_usage_events": { + "name": "billing_usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "billing_usage_events_idempotency_key_index": { + "name": "billing_usage_events_idempotency_key_index", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_project_created_at_index": { + "name": "billing_usage_events_project_created_at_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_sync_status_created_at_index": { + "name": "billing_usage_events_sync_status_created_at_index", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_resource_index": { + "name": "billing_usage_events_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_assignments": { + "name": "ip_assignments", + "schema": "", + "columns": { + "ip": { + "name": "ip", + "type": "inet", + "primaryKey": false, + "notNull": true + }, + "ip_block_id": { + "name": "ip_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ip_assignments_ip_block_id_index": { + "name": "ip_assignments_ip_block_id_index", + "columns": [ + { + "expression": "ip_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ip_assignments_associated_vm_id_index": { + "name": "ip_assignments_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ip_assignments_ip_block_id_ip_blocks_id_fk": { + "name": "ip_assignments_ip_block_id_ip_blocks_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "ip_blocks", + "columnsFrom": [ + "ip_block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ip_assignments_associated_vm_id_vms_id_fk": { + "name": "ip_assignments_associated_vm_id_vms_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_blocks": { + "name": "ip_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ip_block": { + "name": "ip_block", + "type": "cidr", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_allocations": { + "name": "ipam_allocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ipam_prefix_id": { + "name": "ipam_prefix_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "cidr", + "primaryKey": false, + "notNull": false + }, + "prefix_length": { + "name": "prefix_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_allocations_ipam_prefix_id_index": { + "name": "ipam_allocations_ipam_prefix_id_index", + "columns": [ + { + "expression": "ipam_prefix_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_associated_vm_id_index": { + "name": "ipam_allocations_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_vm_family_index": { + "name": "ipam_allocations_vm_family_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_address_index": { + "name": "ipam_allocations_address_index", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_prefix_index": { + "name": "ipam_allocations_prefix_index", + "columns": [ + { + "expression": "prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk": { + "name": "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "ipam_prefixes", + "columnsFrom": [ + "ipam_prefix_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ipam_allocations_associated_vm_id_vms_id_fk": { + "name": "ipam_allocations_associated_vm_id_vms_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_prefixes": { + "name": "ipam_prefixes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cidr": { + "name": "cidr", + "type": "cidr", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ipv6_use_transit_address": { + "name": "ipv6_use_transit_address", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelist_start": { + "name": "whitelist_start", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "whitelist_end": { + "name": "whitelist_end", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "gateway_address": { + "name": "gateway_address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_prefixes_cidr_index": { + "name": "ipam_prefixes_cidr_index", + "columns": [ + { + "expression": "cidr", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_prefixes_family_disabled_index": { + "name": "ipam_prefixes_family_disabled_index", + "columns": [ + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "disabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.managed_hosts": { + "name": "managed_hosts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_kind": { + "name": "host_kind", + "type": "managed_host_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'external'" + }, + "linked_vm_id": { + "name": "linked_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_mode": { + "name": "connection_mode", + "type": "managed_host_connection_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'direct_http'" + }, + "connection_state": { + "name": "connection_state", + "type": "managed_host_connection_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "agent_url": { + "name": "agent_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bearer_token": { + "name": "bearer_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "arch": { + "name": "arch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "managed_hosts_owner_project_id_index": { + "name": "managed_hosts_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "managed_hosts_linked_vm_id_index": { + "name": "managed_hosts_linked_vm_id_index", + "columns": [ + { + "expression": "linked_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "managed_hosts_connection_state_index": { + "name": "managed_hosts_connection_state_index", + "columns": [ + { + "expression": "connection_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "managed_hosts_owner_project_id_organization_id_fk": { + "name": "managed_hosts_owner_project_id_organization_id_fk", + "tableFrom": "managed_hosts", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "managed_hosts_linked_vm_id_vms_id_fk": { + "name": "managed_hosts_linked_vm_id_vms_id_fk", + "tableFrom": "managed_hosts", + "tableTo": "vms", + "columnsFrom": [ + "linked_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_periods": { + "name": "payment_periods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "vm_id": { + "name": "vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payment_periods_vm_id_index": { + "name": "payment_periods_vm_id_index", + "columns": [ + { + "expression": "vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_periods_user_id_index": { + "name": "payment_periods_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_periods_vm_id_vms_id_fk": { + "name": "payment_periods_vm_id_vms_id_fk", + "tableFrom": "payment_periods", + "tableTo": "vms", + "columnsFrom": [ + "vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_billing_customers": { + "name": "project_billing_customers", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "autumn_customer_id": { + "name": "autumn_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "project_billing_customers_autumn_customer_id_index": { + "name": "project_billing_customers_autumn_customer_id_index", + "columns": [ + { + "expression": "autumn_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_keys": { + "name": "ssh_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ssh_keys_user_id_index": { + "name": "ssh_keys_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vm_types": { + "name": "vm_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cores": { + "name": "cores", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ram_capacity": { + "name": "ram_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storage_amount": { + "name": "storage_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "autumn_feature_id": { + "name": "autumn_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vms": { + "name": "vms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proxmox_id": { + "name": "proxmox_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "proxmox_node": { + "name": "proxmox_node", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv4": { + "name": "last_known_ipv4", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv6": { + "name": "last_known_ipv6", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_status": { + "name": "last_known_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_uptime": { + "name": "last_known_uptime", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_known_at": { + "name": "last_known_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vm_type_id": { + "name": "vm_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + }, + "backend": { + "name": "backend", + "type": "vm_backend", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "vm_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'provisioning'" + }, + "status_error": { + "name": "status_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "vms_owner_project_id_index": { + "name": "vms_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_owner_project_active_index": { + "name": "vms_owner_project_active_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_proxmox_id_index": { + "name": "vms_proxmox_id_index", + "columns": [ + { + "expression": "proxmox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vms_owner_project_id_organization_id_fk": { + "name": "vms_owner_project_id_organization_id_fk", + "tableFrom": "vms", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vms_vm_type_id_vm_types_id_fk": { + "name": "vms_vm_type_id_vm_types_id_fk", + "tableFrom": "vms", + "tableTo": "vm_types", + "columnsFrom": [ + "vm_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volumes": { + "name": "volumes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "volumes_owner_project_id_index": { + "name": "volumes_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "volumes_associated_vm_id_index": { + "name": "volumes_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "volumes_owner_project_id_organization_id_fk": { + "name": "volumes_owner_project_id_organization_id_fk", + "tableFrom": "volumes", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "volumes_associated_vm_id_vms_id_fk": { + "name": "volumes_associated_vm_id_vms_id_fk", + "tableFrom": "volumes", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.billing_resource_type": { + "name": "billing_resource_type", + "schema": "public", + "values": [ + "vm", + "volume" + ] + }, + "public.billing_sync_status": { + "name": "billing_sync_status", + "schema": "public", + "values": [ + "pending", + "synced", + "failed", + "abandoned" + ] + }, + "public.ip_family": { + "name": "ip_family", + "schema": "public", + "values": [ + "ipv4", + "ipv6" + ] + }, + "public.managed_host_connection_mode": { + "name": "managed_host_connection_mode", + "schema": "public", + "values": [ + "direct_http", + "websocket", + "vsock_gateway", + "offline" + ] + }, + "public.managed_host_connection_state": { + "name": "managed_host_connection_state", + "schema": "public", + "values": [ + "online", + "offline", + "unknown" + ] + }, + "public.managed_host_kind": { + "name": "managed_host_kind", + "schema": "public", + "values": [ + "stack_vps", + "external", + "local" + ] + }, + "public.vm_backend": { + "name": "vm_backend", + "schema": "public", + "values": [ + "proxmox" + ] + }, + "public.vm_isa": { + "name": "vm_isa", + "schema": "public", + "values": [ + "x86", + "arm", + "risc-v" + ] + }, + "public.vm_status": { + "name": "vm_status", + "schema": "public", + "values": [ + "provisioning", + "ready", + "error", + "deleting" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dashboard/drizzle/meta/_journal.json b/apps/dashboard/drizzle/meta/_journal.json index 965ec8a..66de60c 100644 --- a/apps/dashboard/drizzle/meta/_journal.json +++ b/apps/dashboard/drizzle/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1784357065417, "tag": "0027_adorable_bedlam", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1784413794152, + "tag": "0028_lame_james_howlett", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/dashboard/src/app.d.ts b/apps/dashboard/src/app.d.ts index ca07deb..a88df8e 100644 --- a/apps/dashboard/src/app.d.ts +++ b/apps/dashboard/src/app.d.ts @@ -24,6 +24,7 @@ declare global { ctx: ExecutionContext; env: { ORIGIN: string; + PUBLIC_DASHBOARD_BRAND?: string; BETTER_AUTH_SECRET: string; VYOS_API_URL?: string; VYOS_API_KEY?: string; @@ -73,6 +74,7 @@ declare global { colocation: boolean; firewall: boolean; images: boolean; + managedHosts: boolean; volumes: boolean; }; } diff --git a/apps/dashboard/src/lib/branding.ts b/apps/dashboard/src/lib/branding.ts new file mode 100644 index 0000000..c11150b --- /dev/null +++ b/apps/dashboard/src/lib/branding.ts @@ -0,0 +1,32 @@ +export type DashboardBrandId = 'stack' | 'ultramarine'; + +const requestedBrand = import.meta.env.PUBLIC_DASHBOARD_BRAND?.toLowerCase(); +export const dashboardBrandId: DashboardBrandId = + requestedBrand === 'ultramarine' ? 'ultramarine' : 'stack'; + +export const dashboardBrand = { + id: dashboardBrandId, + name: dashboardBrandId === 'ultramarine' ? 'Ultramarine' : 'Stack', + title: dashboardBrandId === 'ultramarine' ? 'Ultramarine Server' : 'Stack', + logo: dashboardBrandId === 'ultramarine' ? '/ultramarine-logo.svg' : '/logo.svg', + favicon: dashboardBrandId === 'ultramarine' ? '/ultramarine-favicon.ico' : '/favicon.ico', + favicon16: + dashboardBrandId === 'ultramarine' + ? '/ultramarine-favicon-16x16.png' + : '/favicon-16x16.png', + favicon32: + dashboardBrandId === 'ultramarine' + ? '/ultramarine-favicon-32x32.png' + : '/favicon-32x32.png', + appleTouchIcon: + dashboardBrandId === 'ultramarine' + ? '/ultramarine-apple-touch-icon.png' + : '/apple-touch-icon.png', + isStandalone: dashboardBrandId === 'ultramarine', + defaultProjectPath: dashboardBrandId === 'ultramarine' ? 'hosts' : 'servers', + plausibleDomain: dashboardBrandId === 'ultramarine' ? null : 'dash.fyrastack.com' +} as const; + +export function pageTitle(title: string) { + return `${title} / ${dashboardBrand.title}`; +} diff --git a/apps/dashboard/src/lib/components/dialogs/totp-onboarding-dialog.svelte b/apps/dashboard/src/lib/components/dialogs/totp-onboarding-dialog.svelte index d749fb8..b022fd8 100644 --- a/apps/dashboard/src/lib/components/dialogs/totp-onboarding-dialog.svelte +++ b/apps/dashboard/src/lib/components/dialogs/totp-onboarding-dialog.svelte @@ -8,6 +8,7 @@ import Check from '~icons/lucide/check'; import Copy from '~icons/nucleo/copy'; import ShieldCheck from '~icons/nucleo/shield-check'; + import { dashboardBrand } from '$lib/branding'; type Props = { open?: boolean; @@ -62,7 +63,7 @@ verifyError = ''; const { data, error } = await authClient.twoFactor.enable({ password: setupPassword, - issuer: 'Fyra Stack' + issuer: dashboardBrand.title }); setupSubmitting = false; if (error) { diff --git a/apps/dashboard/src/lib/feature-flags.ts b/apps/dashboard/src/lib/feature-flags.ts index a9e4516..e795c3a 100644 --- a/apps/dashboard/src/lib/feature-flags.ts +++ b/apps/dashboard/src/lib/feature-flags.ts @@ -2,6 +2,7 @@ export const featureFlagKeys = [ 'colocation', 'firewall', 'images', + 'managedHosts', 'volumes', 'vpsConsole', 'vpsLogs', @@ -23,6 +24,7 @@ export const defaultFeatureFlags: FeatureFlags = { colocation: false, firewall: false, images: false, + managedHosts: true, volumes: false, vpsConsole: true, vpsLogs: true, @@ -40,6 +42,7 @@ export const developmentFeatureFlags: FeatureFlags = { colocation: true, firewall: true, images: true, + managedHosts: true, volumes: true, vpsConsole: true, vpsLogs: true, @@ -57,6 +60,7 @@ export const featureFlagLabels: Record = { colocation: 'Colocation', firewall: 'Firewall', images: 'Images', + managedHosts: 'Managed Hosts', volumes: 'Volumes', vpsConsole: 'Console Tab', vpsLogs: 'Logs Tab', @@ -74,6 +78,7 @@ export const featureFlagDescriptions: Record = { colocation: 'Enable colocation server management', firewall: 'Enable firewall rule management for projects', images: 'Enable custom image management and deployment', + managedHosts: 'Enable Tetra-managed host enrollment and control', volumes: 'Enable persistent volume management', vpsConsole: 'Web-based serial console access for VPS instances', vpsLogs: 'System and service log viewing for VPS instances', @@ -90,7 +95,7 @@ export const featureFlagDescriptions: Record = { export type FeatureFlagCategory = 'platform' | 'server'; export const featureFlagCategories: Record = { - platform: ['colocation', 'firewall', 'images', 'volumes'], + platform: ['colocation', 'firewall', 'images', 'managedHosts', 'volumes'], server: [ 'vpsConsole', 'vpsLogs', diff --git a/apps/dashboard/src/lib/remote/feature-flags.remote.ts b/apps/dashboard/src/lib/remote/feature-flags.remote.ts index 653be9f..cc2bfca 100644 --- a/apps/dashboard/src/lib/remote/feature-flags.remote.ts +++ b/apps/dashboard/src/lib/remote/feature-flags.remote.ts @@ -7,7 +7,7 @@ import { setFeatureFlag } from '$lib/server/feature-flags'; import type { FeatureFlagKey } from '$lib/feature-flags'; const featureFlagParam = - "'colocation' | 'firewall' | 'images' | 'volumes' | 'vpsConsole' | 'vpsLogs' | 'vpsNetworking' | 'vpsImages' | 'vpsSnapshots' | 'vpsBackups' | 'vpsRebuild' | 'vpsResize' | 'vpsRescue' | 'vpsSettings'"; + "'colocation' | 'firewall' | 'images' | 'managedHosts' | 'volumes' | 'vpsConsole' | 'vpsLogs' | 'vpsNetworking' | 'vpsImages' | 'vpsSnapshots' | 'vpsBackups' | 'vpsRebuild' | 'vpsResize' | 'vpsRescue' | 'vpsSettings'"; const updateFeatureFlagParams = type({ flag: featureFlagParam, diff --git a/apps/dashboard/src/lib/remote/managed-hosts.remote.ts b/apps/dashboard/src/lib/remote/managed-hosts.remote.ts new file mode 100644 index 0000000..d344958 --- /dev/null +++ b/apps/dashboard/src/lib/remote/managed-hosts.remote.ts @@ -0,0 +1,242 @@ +import { command, getRequestEvent, query } from '$app/server'; +import { error } from '@sveltejs/kit'; +import { type } from 'arktype'; +import { and, desc, eq } from 'drizzle-orm'; +import { requireProjectAccess } from '$lib/server/auth-context'; +import { initDrizzle } from '$lib/server/db'; +import { managedHosts } from '$lib/server/db/schema'; +import { createTetraClient, type AgentResponse } from '$lib/server/tetra/client'; + +export type ManagedHost = { + id: string; + displayName: string; + ownerProjectId: string; + hostKind: 'stack_vps' | 'external' | 'local'; + linkedVmId: string | null; + connectionMode: 'direct_http' | 'websocket' | 'vsock_gateway' | 'offline'; + connectionState: 'online' | 'offline' | 'unknown'; + agentUrl: string | null; + lastSeenAt: number | null; + agentVersion: string | null; + hostname: string | null; + os: string | null; + arch: string | null; + capabilities: Record | null; + lastError: string | null; + createdAt: number; + updatedAt: number; +}; + +function requireUser() { + const event = getRequestEvent(); + if (!event?.locals.user) error(401, 'Authentication required'); + return event.locals.user; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function mapHost(row: typeof managedHosts.$inferSelect): ManagedHost { + return { + id: row.id, + displayName: row.displayName, + ownerProjectId: row.ownerProjectId, + hostKind: row.hostKind, + linkedVmId: row.linkedVmId, + connectionMode: row.connectionMode, + connectionState: row.connectionState, + agentUrl: row.agentUrl, + lastSeenAt: row.lastSeenAt, + agentVersion: row.agentVersion, + hostname: row.hostname, + os: row.os, + arch: row.arch, + capabilities: row.capabilities, + lastError: row.lastError, + createdAt: row.createdAt, + updatedAt: row.updatedAt + }; +} + +async function loadManagedHost(hostId: string) { + const user = requireUser(); + const db = initDrizzle(); + const host = await db.query.managedHosts.findFirst({ + where: eq(managedHosts.id, hostId) + }); + + if (!host) error(404, 'Managed host not found'); + + await requireProjectAccess(db, user.id, host.ownerProjectId); + return { db, host }; +} + +async function refreshHostCapabilities(host: typeof managedHosts.$inferSelect) { + const client = createTetraClient({ + connectionMode: host.connectionMode, + agentUrl: host.agentUrl, + bearerToken: host.bearerToken + }); + + await client.health(); + const capabilities = await client.capabilities(); + let system: AgentResponse | null = null; + + try { + system = await client.dispatch({ + module: 'settings', + action: 'get_system', + payload: {} + }); + } catch { + system = null; + } + + return { + capabilities: isRecord(capabilities.payload) ? capabilities.payload : null, + system: isRecord(system?.payload) ? system.payload : null + }; +} + +const listParams = type({ projectId: 'string' }); +export const listManagedHosts = query(listParams, async (params): Promise => { + const user = requireUser(); + const db = initDrizzle(); + await requireProjectAccess(db, user.id, params.projectId); + + const rows = await db.query.managedHosts.findMany({ + where: eq(managedHosts.ownerProjectId, params.projectId), + orderBy: [desc(managedHosts.createdAt)] + }); + + return rows.map(mapHost); +}); + +const getParams = type({ hostId: 'string' }); +export const getManagedHost = query(getParams, async (params): Promise => { + const { host } = await loadManagedHost(params.hostId); + return mapHost(host); +}); + +const createParams = type({ + projectId: 'string', + displayName: 'string', + agentUrl: 'string', + bearerToken: 'string?', + linkedVmId: 'string?' +}); +export const createManagedHost = command(createParams, async (params): Promise => { + const user = requireUser(); + const db = initDrizzle(); + await requireProjectAccess(db, user.id, params.projectId, 'admin'); + + const displayName = params.displayName.trim(); + if (!displayName) error(400, 'Display name is required'); + + let agentUrl: string; + try { + agentUrl = new URL(params.agentUrl).toString().replace(/\/+$/, ''); + } catch { + error(400, 'Agent URL must be a valid URL'); + } + + const [host] = await db + .insert(managedHosts) + .values({ + displayName, + ownerProjectId: params.projectId, + hostKind: params.linkedVmId ? 'stack_vps' : 'external', + linkedVmId: params.linkedVmId ?? null, + connectionMode: 'direct_http', + connectionState: 'unknown', + agentUrl, + bearerToken: params.bearerToken?.trim() || null + }) + .returning(); + + return mapHost(host); +}); + +export const refreshManagedHostCapabilities = command( + getParams, + async (params): Promise => { + const { db, host } = await loadManagedHost(params.hostId); + + try { + const result = await refreshHostCapabilities(host); + const now = Date.now(); + const [updated] = await db + .update(managedHosts) + .set({ + connectionState: 'online', + lastSeenAt: now, + capabilities: result.capabilities, + os: typeof result.system?.os === 'string' ? result.system.os : host.os, + arch: typeof result.system?.arch === 'string' ? result.system.arch : host.arch, + lastError: null, + updatedAt: now + }) + .where(eq(managedHosts.id, host.id)) + .returning(); + + return mapHost(updated); + } catch (err) { + const now = Date.now(); + const [updated] = await db + .update(managedHosts) + .set({ + connectionState: 'offline', + lastError: err instanceof Error ? err.message : 'Failed to refresh host capabilities', + updatedAt: now + }) + .where(eq(managedHosts.id, host.id)) + .returning(); + + return mapHost(updated); + } + } +); + +const dispatchParams = type({ + hostId: 'string', + module: 'string', + action: 'string', + payloadJson: 'string' +}); +export const dispatchManagedHostCommand = command(dispatchParams, async (params) => { + const user = requireUser(); + const { db, host } = await loadManagedHost(params.hostId); + await requireProjectAccess(db, user.id, host.ownerProjectId, 'admin'); + + let payload: Record; + try { + const parsed = JSON.parse(params.payloadJson); + payload = isRecord(parsed) ? parsed : {}; + } catch { + error(400, 'Payload must be valid JSON'); + } + + const client = createTetraClient({ + connectionMode: host.connectionMode, + agentUrl: host.agentUrl, + bearerToken: host.bearerToken + }); + const response = await client.dispatch({ + module: params.module.trim(), + action: params.action.trim(), + payload + }); + + await db + .update(managedHosts) + .set({ + connectionState: 'online', + lastSeenAt: Date.now(), + lastError: response.ok ? null : response.error || null, + updatedAt: Date.now() + }) + .where(and(eq(managedHosts.id, host.id), eq(managedHosts.ownerProjectId, host.ownerProjectId))); + + return response; +}); diff --git a/apps/dashboard/src/lib/server/auth.ts b/apps/dashboard/src/lib/server/auth.ts index ea67434..75cbb76 100644 --- a/apps/dashboard/src/lib/server/auth.ts +++ b/apps/dashboard/src/lib/server/auth.ts @@ -20,6 +20,7 @@ import { sendSecurityAlertEmail } from '$lib/server/email-notifications'; import { updateProjectCustomer } from '$lib/server/billing/autumn'; import { getRuntimeEnv } from '$lib/server/env'; import { ulid } from '$lib/server/id'; +import { dashboardBrand } from '$lib/branding'; const PENDING_PASSKEY_COOKIE = 'pending_passkey_2fa'; const PENDING_PASSKEY_HINT_COOKIE = 'pending_passkey_2fa_hint'; @@ -121,7 +122,7 @@ function buildAuth() { const baseURL = dev ? getRequestEvent().url.origin : env.ORIGIN; return betterAuth({ - appName: 'Stack', + appName: dashboardBrand.title, baseURL, secret: env.BETTER_AUTH_SECRET, database: drizzleAdapter(db, { provider: 'pg' }), diff --git a/apps/dashboard/src/lib/server/db/schema.ts b/apps/dashboard/src/lib/server/db/schema.ts index 1caacb0..2ccff10 100644 --- a/apps/dashboard/src/lib/server/db/schema.ts +++ b/apps/dashboard/src/lib/server/db/schema.ts @@ -7,6 +7,7 @@ import { integer, numeric, date, + jsonb, index, uniqueIndex, inet, @@ -27,6 +28,21 @@ export const vmBackendEnum = pgEnum('vm_backend', ['proxmox']); export const vmStatusEnum = pgEnum('vm_status', ['provisioning', 'ready', 'error', 'deleting']); +export const managedHostKindEnum = pgEnum('managed_host_kind', ['stack_vps', 'external', 'local']); + +export const managedHostConnectionModeEnum = pgEnum('managed_host_connection_mode', [ + 'direct_http', + 'websocket', + 'vsock_gateway', + 'offline' +]); + +export const managedHostConnectionStateEnum = pgEnum('managed_host_connection_state', [ + 'online', + 'offline', + 'unknown' +]); + export const billingSyncStatusEnum = pgEnum('billing_sync_status', [ 'pending', 'synced', @@ -102,11 +118,64 @@ export const vmsRelations = relations(vms, ({ one, many }) => ({ references: [vmTypes.id] }), volumes: many(volumes), + managedHosts: many(managedHosts), paymentPeriods: many(paymentPeriods), ipAssignments: many(ipAssignments), ipamAllocations: many(ipamAllocations) })); +// Managed Hosts + +export const managedHosts = pgTable( + 'managed_hosts', + { + id: ulidPk(), + displayName: text('display_name').notNull(), + ownerProjectId: ulidFk('owner_project_id') + .notNull() + .references(() => organization.id), + hostKind: managedHostKindEnum('host_kind').notNull().default('external'), + linkedVmId: ulidFk('linked_vm_id').references(() => vms.id), + connectionMode: managedHostConnectionModeEnum('connection_mode') + .notNull() + .default('direct_http'), + connectionState: managedHostConnectionStateEnum('connection_state') + .notNull() + .default('unknown'), + agentUrl: text('agent_url'), + bearerToken: text('bearer_token'), + lastSeenAt: bigint('last_seen_at', { mode: 'number' }), + agentVersion: text('agent_version'), + hostname: text('hostname'), + os: text('os'), + arch: text('arch'), + capabilities: jsonb('capabilities').$type | null>(), + lastError: text('last_error'), + createdAt: bigint('created_at', { mode: 'number' }) + .notNull() + .default(sql`(extract(epoch from now()) * 1000)::bigint`), + updatedAt: bigint('updated_at', { mode: 'number' }) + .notNull() + .default(sql`(extract(epoch from now()) * 1000)::bigint`) + }, + (table) => [ + index('managed_hosts_owner_project_id_index').on(table.ownerProjectId), + index('managed_hosts_linked_vm_id_index').on(table.linkedVmId), + index('managed_hosts_connection_state_index').on(table.connectionState) + ] +); + +export const managedHostsRelations = relations(managedHosts, ({ one }) => ({ + project: one(organization, { + fields: [managedHosts.ownerProjectId], + references: [organization.id] + }), + vm: one(vms, { + fields: [managedHosts.linkedVmId], + references: [vms.id] + }) +})); + // Volumes export const volumes = pgTable( diff --git a/apps/dashboard/src/lib/server/tetra/client.ts b/apps/dashboard/src/lib/server/tetra/client.ts new file mode 100644 index 0000000..6d10797 --- /dev/null +++ b/apps/dashboard/src/lib/server/tetra/client.ts @@ -0,0 +1,98 @@ +import { ulid } from '$lib/server/id'; + +export type AgentCommand = { + id: string; + module: string; + action: string; + payload: Record; + signature?: string | null; +}; + +export type AgentResponse = { + id: string; + ok: boolean; + payload?: unknown; + error?: string; +}; + +export type TetraClient = { + health(): Promise<{ ok: boolean }>; + capabilities(): Promise; + dispatch(command: Omit & { id?: string }): Promise; +}; + +export class DirectHttpTetraClient implements TetraClient { + readonly #baseUrl: string; + readonly #bearerToken: string | null; + + constructor(baseUrl: string, bearerToken?: string | null) { + this.#baseUrl = baseUrl.replace(/\/+$/, ''); + this.#bearerToken = bearerToken?.trim() || null; + } + + async health() { + return this.#request<{ ok: boolean }>('/health'); + } + + async capabilities() { + return this.#request('/capabilities'); + } + + async dispatch(command: Omit & { id?: string }) { + return this.#request('/dispatch', { + method: 'POST', + body: JSON.stringify({ + id: command.id ?? `cmd-${ulid()}`, + module: command.module, + action: command.action, + payload: command.payload, + signature: command.signature ?? null + }) + }); + } + + async #request(path: string, init: RequestInit = {}): Promise { + const headers = new Headers(init.headers); + headers.set('accept', 'application/json'); + + if (init.body && !headers.has('content-type')) { + headers.set('content-type', 'application/json'); + } + + if (this.#bearerToken) { + headers.set('authorization', `Bearer ${this.#bearerToken}`); + } + + const response = await fetch(`${this.#baseUrl}${path}`, { + ...init, + headers + }); + const text = await response.text(); + const data = text ? JSON.parse(text) : null; + + if (!response.ok) { + const message = + (data && typeof data === 'object' && 'error' in data ? String(data.error) : '') || + `Tetra request failed with HTTP ${response.status}`; + throw new Error(message); + } + + return data as T; + } +} + +export function createTetraClient(options: { + connectionMode: string; + agentUrl: string | null; + bearerToken: string | null; +}): TetraClient { + if (options.connectionMode !== 'direct_http') { + throw new Error(`Tetra connection mode ${options.connectionMode} is not implemented yet`); + } + + if (!options.agentUrl) { + throw new Error('Direct HTTP Tetra hosts require an agent URL'); + } + + return new DirectHttpTetraClient(options.agentUrl, options.bearerToken); +} diff --git a/apps/dashboard/src/routes/(app)/+layout.svelte b/apps/dashboard/src/routes/(app)/+layout.svelte index ccff570..97c7733 100644 --- a/apps/dashboard/src/routes/(app)/+layout.svelte +++ b/apps/dashboard/src/routes/(app)/+layout.svelte @@ -21,6 +21,7 @@ import { untrack } from 'svelte'; import { listVms } from '$lib/remote/vms.remote'; import { authClient } from '$lib/auth-client'; + import { dashboardBrand, pageTitle } from '$lib/branding'; import type { FeatureFlags } from '$lib/feature-flags'; import type { IconComponent } from '$lib'; import ArrowRight from '~icons/lucide/arrow-right'; @@ -88,7 +89,11 @@ const items: { icon: IconComponent; label: string; href: string }[] = []; if (!currentProject) return items; const prefix = `/projects/${currentProject.id}`; - items.push({ icon: Server, label: 'Servers', href: `${prefix}/servers` }); + if (featureFlags.managedHosts) + items.push({ icon: Server, label: 'Managed Hosts', href: `${prefix}/hosts` }); + if (!dashboardBrand.isStandalone) { + items.push({ icon: Server, label: 'Servers', href: `${prefix}/servers` }); + } if (featureFlags.colocation) items.push({ icon: Warehouse, label: 'Colocation', href: `${prefix}/colocation` }); if (featureFlags.volumes) @@ -96,7 +101,9 @@ if (featureFlags.firewall) items.push({ icon: Shield, label: 'Firewall', href: `${prefix}/firewall` }); if (featureFlags.images) items.push({ icon: Disc, label: 'Images', href: `${prefix}/images` }); - items.push({ icon: CreditCard, label: 'Billing', href: `${prefix}/billing` }); + if (!dashboardBrand.isStandalone) { + items.push({ icon: CreditCard, label: 'Billing', href: `${prefix}/billing` }); + } items.push({ icon: Settings, label: 'Settings', href: `${prefix}/settings` }); return items; }); @@ -130,7 +137,7 @@ try { selectedProjectId = projectId; await authClient.organization.setActive({ organizationId: projectId }); - await goto(resolve(`/projects/${projectId}/servers`)); + await goto(resolve(`/projects/${projectId}/${dashboardBrand.defaultProjectPath}`)); } catch (error) { toast.error(getErrorMessage(error, 'Failed to switch project')); } finally { @@ -196,13 +203,19 @@ { id: 'all', label: 'All', icon: Search }, { id: 'navigate', label: 'Pages', icon: ArrowRight } ]; - if (showServerFilter) filters.push({ id: 'servers', label: 'Servers', icon: Server }); + if (!dashboardBrand.isStandalone && showServerFilter) { + filters.push({ id: 'servers', label: 'Servers', icon: Server }); + } filters.push({ id: 'account', label: 'Account', icon: User }); return filters; }); const navigateCommands = $derived.by(() => { - const commands: CommandEntry[] = [{ icon: Server, label: 'Servers', href: '/servers' }]; + const commands: CommandEntry[] = []; + if (showManagedHosts) commands.push({ icon: Server, label: 'Managed Hosts', href: '/hosts' }); + if (!dashboardBrand.isStandalone) { + commands.push({ icon: Server, label: 'Servers', href: '/servers' }); + } if (showColocation) commands.push({ icon: Warehouse, label: 'Colocation', href: '/colocation' }); if (showVolumes) commands.push({ icon: HardDrive, label: 'Volumes', href: '/volumes' }); @@ -248,6 +261,7 @@ } async function loadCommandServers(projectId = selectedProjectId) { + if (dashboardBrand.isStandalone) return; if (!projectId || commandServersLoading || commandServersLoadedProjectId === projectId) return; const requestId = ++commandServersRequestId; commandServersLoading = true; @@ -286,7 +300,7 @@ commandSearch = ''; cmdFilter = 'all'; commandOpen = true; - void loadCommandServers(); + if (!dashboardBrand.isStandalone) void loadCommandServers(); } $effect(() => { @@ -327,6 +341,7 @@ } const showColocation = $derived(!!featureFlags.colocation); + const showManagedHosts = $derived(!!featureFlags.managedHosts); const showVolumes = $derived(!!featureFlags.volumes); const showFirewall = $derived(!!featureFlags.firewall); const showImages = $derived(!!featureFlags.images); @@ -337,7 +352,7 @@ - Stack / Dashboard + {pageTitle('Dashboard')} {#if !data.user} @@ -356,8 +371,10 @@ {/if} - Stack - Stack + {dashboardBrand.name} + + {dashboardBrand.name} + {#if isOnProjectRoute} / @@ -381,16 +398,16 @@ > {project.projectName} {#if switchingProjectId === project.id} - + {:else if selectedProjectId === project.id} - + {/if} {/each} @@ -475,7 +492,7 @@ class="flex h-8 w-8 items-center justify-center transition-colors duration-100 {isActive( item.href ) - ? 'border border-red-500 text-foreground' + ? 'border border-primary text-foreground' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'}" > @@ -542,12 +559,12 @@ > {project.projectName} {#if selectedProjectId === project.id} - + {/if} {/each} diff --git a/apps/dashboard/src/routes/(app)/+page.svelte b/apps/dashboard/src/routes/(app)/+page.svelte index 4fcbeca..40d85b2 100644 --- a/apps/dashboard/src/routes/(app)/+page.svelte +++ b/apps/dashboard/src/routes/(app)/+page.svelte @@ -20,6 +20,7 @@ import { toast } from 'svelte-sonner'; import { getErrorMessage } from '$lib/utils'; import { isProjectRole, projectRoleLabels } from '$lib/auth/organization-permissions'; + import { dashboardBrand, pageTitle } from '$lib/branding'; type Project = { id: string; projectName: string; role: string }; @@ -48,7 +49,7 @@ newProjectName = ''; createOpen = false; await authClient.organization.setActive({ organizationId: res.id }); - await goto(`/projects/${res.id}/servers`); + await goto(`/projects/${res.id}/${dashboardBrand.defaultProjectPath}`); } catch (err) { createProjectError = getErrorMessage(err, 'Project could not be created.'); } finally { @@ -56,7 +57,7 @@ } } - async function openProject(project: Project, path = 'servers') { + async function openProject(project: Project, path: string = dashboardBrand.defaultProjectPath) { await authClient.organization.setActive({ organizationId: project.id }); await goto(`/projects/${project.id}/${path}`); } @@ -109,7 +110,7 @@ - Projects / Stack + {pageTitle('Projects')}
@@ -172,7 +173,7 @@
diff --git a/apps/dashboard/src/routes/(app)/admin/features/+page.svelte b/apps/dashboard/src/routes/(app)/admin/features/+page.svelte index 122812a..21befb1 100644 --- a/apps/dashboard/src/routes/(app)/admin/features/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/features/+page.svelte @@ -35,6 +35,7 @@ colocation: Server, firewall: Shield, images: Image, + managedHosts: Server, volumes: HardDrive, vpsConsole: Terminal, vpsLogs: FileText, diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/+page.server.ts b/apps/dashboard/src/routes/(app)/projects/[projectid]/+page.server.ts index 81e01b4..62e1ef7 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/+page.server.ts +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/+page.server.ts @@ -1,6 +1,7 @@ import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; +import { dashboardBrand } from '$lib/branding'; export const load: PageServerLoad = async ({ params }) => { - throw redirect(303, `/projects/${params.projectid}/servers`); + throw redirect(303, `/projects/${params.projectid}/${dashboardBrand.defaultProjectPath}`); }; diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/+page.server.ts b/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/+page.server.ts new file mode 100644 index 0000000..9e289cf --- /dev/null +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/+page.server.ts @@ -0,0 +1,20 @@ +import type { PageServerLoad } from './$types'; +import { error } from '@sveltejs/kit'; +import { listManagedHosts } from '$lib/remote/managed-hosts.remote'; + +export const load: PageServerLoad = async ({ params, parent, depends }) => { + depends('project:managed-hosts'); + const { featureFlags } = await parent(); + + if (!featureFlags.managedHosts) { + error(404, 'Not found'); + } + + if (!params.projectid) { + error(404, 'Project not found'); + } + + return { + hosts: await listManagedHosts({ projectId: params.projectid }) + }; +}; diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/+page.svelte new file mode 100644 index 0000000..3f554ce --- /dev/null +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/+page.svelte @@ -0,0 +1,216 @@ + + +
+
+
+

Managed Hosts

+

+ Register Tetra agents and inspect host capabilities. +

+
+
+ +
+
+
+
+

Register Host

+

+ Use a Tetra dev HTTP agent while the production WSS broker is being built. +

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {#if actionError} +

+ {actionError} +

+ {/if} + + +
+
+ +
+ {#if hosts.length === 0} +
+
+ +

No managed hosts

+

+ Register a Tetra agent to start testing host discovery. +

+
+
+ {:else} +
+ {#each hosts as host (host.id)} +
+ + +
+ +
+
+ {/each} +
+ {/if} +
+
+
diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/[id]/+page.server.ts b/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/[id]/+page.server.ts new file mode 100644 index 0000000..32cacfc --- /dev/null +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/[id]/+page.server.ts @@ -0,0 +1,21 @@ +import type { PageServerLoad } from './$types'; +import { error } from '@sveltejs/kit'; +import { getManagedHost } from '$lib/remote/managed-hosts.remote'; + +export const load: PageServerLoad = async ({ params, parent, depends }) => { + depends(`managed-host:${params.id}`); + const { featureFlags } = await parent(); + + if (!featureFlags.managedHosts) { + error(404, 'Not found'); + } + + if (!params.projectid) { + error(404, 'Project not found'); + } + + const host = await getManagedHost({ hostId: params.id }); + if (host.ownerProjectId !== params.projectid) error(404, 'Managed host not found'); + + return { host }; +}; diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/[id]/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/[id]/+page.svelte new file mode 100644 index 0000000..2d14c8a --- /dev/null +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/hosts/[id]/+page.svelte @@ -0,0 +1,192 @@ + + +
+
+
+
+

{host.displayName}

+ {host.connectionState} +
+

{host.agentUrl ?? 'No agent URL'}

+
+ +
+ +
+
+
+

Host Details

+

+ Current dashboard state for this Tetra agent. +

+
+ +
+ {#each [['Kind', host.hostKind], ['Mode', host.connectionMode], ['Last seen', formatDate(host.lastSeenAt)], ['OS', host.os ?? 'Unknown'], ['Arch', host.arch ?? 'Unknown'], ['Agent', host.agentVersion ?? 'Unknown']] as [label, value] (label)} +
+ {label} + {value} +
+ {/each} +
+ + {#if host.lastError} +
+ {host.lastError} +
+ {/if} + + {#if actionError} +
+ {actionError} +
+ {/if} +
+ +
+
+
+
+
+

Capabilities

+

+ Last successful `agent.capabilities` response. +

+
+
+
{formatJson(
+							host.capabilities
+						)}
+
+ +
+
+

Dispatch Command

+

+ Send a Tetra command envelope through the dashboard server. +

+
+ +
+
+
+ + +
+
+ + +
+
+ +
+ +