dashboards: migrate from filesystem to postgresql

Replace JSON file storage with DB tables for dashboard layouts,
screens, projects, and terminal defaults. Fresh drizzle migration
with dashboardDefaults table and new columns on screens/projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 11:35:11 +00:00
co-authored by Claude Opus 4.6
parent daf5580c39
commit 5129f7827f
12 changed files with 871 additions and 625 deletions
@@ -48,10 +48,8 @@ CREATE TABLE "user_integrations" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"provider" text NOT NULL,
"access_token" text,
"refresh_token" text,
"expires_at" timestamp with time zone,
"profile" jsonb,
"server_integration_id" integer,
"config" jsonb DEFAULT '{}'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_user_integrations_user_provider" UNIQUE("user_id","provider")
@@ -115,28 +113,16 @@ CREATE TABLE "chat_sessions" (
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "projects" (
CREATE TABLE "dashboard_defaults" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"slug" text NOT NULL,
"meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
"layout" jsonb DEFAULT '[]'::jsonb NOT NULL,
"terminals" jsonb DEFAULT '[]'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"terminals" jsonb DEFAULT '{}'::jsonb NOT NULL,
"host_terminals" jsonb DEFAULT '{}'::jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_projects_user_slug" UNIQUE("user_id","slug")
CONSTRAINT "dashboard_defaults_user_id_unique" UNIQUE("user_id")
);
--> statement-breakpoint
CREATE TABLE "screens" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"name" text NOT NULL,
"layout" jsonb DEFAULT '[]'::jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_screens_user_name" UNIQUE("user_id","name")
);
--> statement-breakpoint
CREATE TABLE "workspaces" (
CREATE TABLE "dashboards" (
"id" text PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"name" text NOT NULL,
@@ -147,7 +133,31 @@ CREATE TABLE "workspaces" (
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_workspaces_user_id" UNIQUE("user_id","id")
CONSTRAINT "uq_dashboards_user_id" UNIQUE("user_id","id")
);
--> statement-breakpoint
CREATE TABLE "projects" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"slug" text NOT NULL,
"meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
"layout" jsonb DEFAULT '[]'::jsonb NOT NULL,
"terminals" jsonb DEFAULT '[]'::jsonb NOT NULL,
"host_terminals" jsonb DEFAULT '{}'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_projects_user_slug" UNIQUE("user_id","slug")
);
--> statement-breakpoint
CREATE TABLE "screens" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"name" text NOT NULL,
"layout" jsonb DEFAULT '[]'::jsonb NOT NULL,
"terminals" jsonb DEFAULT '{}'::jsonb NOT NULL,
"host_terminals" jsonb DEFAULT '{}'::jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_screens_user_name" UNIQUE("user_id","name")
);
--> statement-breakpoint
CREATE TABLE "extensions" (
@@ -185,20 +195,6 @@ CREATE TABLE "processes" (
CONSTRAINT "uq_processes_scope_user_dir" UNIQUE("scope","user_id","dir_name")
);
--> statement-breakpoint
CREATE TABLE "resources" (
"id" serial PRIMARY KEY NOT NULL,
"scope" text NOT NULL,
"dir_name" text NOT NULL,
"name" text NOT NULL,
"description" text,
"body" text,
"version" integer DEFAULT 1 NOT NULL,
"config" jsonb DEFAULT '{}'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_resources_scope_dir" UNIQUE("scope","dir_name")
);
--> statement-breakpoint
CREATE TABLE "skills" (
"id" serial PRIMARY KEY NOT NULL,
"scope" text NOT NULL,
@@ -296,18 +292,49 @@ CREATE TABLE "server_config" (
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "server_integrations" (
"id" serial PRIMARY KEY NOT NULL,
"provider" text NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"config" jsonb DEFAULT '{}'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "server_integrations_provider_unique" UNIQUE("provider")
);
--> statement-breakpoint
CREATE TABLE "email_accounts" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer NOT NULL,
"provider" text NOT NULL,
"email" text NOT NULL,
"display_name" text,
"imap_host" text NOT NULL,
"imap_port" integer NOT NULL,
"imap_secure" boolean DEFAULT true NOT NULL,
"auth_type" text NOT NULL,
"credentials" jsonb DEFAULT '{}'::jsonb NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"status" text DEFAULT 'connected' NOT NULL,
"sync_meta" jsonb DEFAULT '{}'::jsonb NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "uq_email_accounts_user_email" UNIQUE("user_id","email")
);
--> statement-breakpoint
ALTER TABLE "passkey_challenges" ADD CONSTRAINT "passkey_challenges_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "passkeys" ADD CONSTRAINT "passkeys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "dock_configs" ADD CONSTRAINT "dock_configs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_integrations" ADD CONSTRAINT "user_integrations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_integrations" ADD CONSTRAINT "user_integrations_server_integration_id_server_integrations_id_fk" FOREIGN KEY ("server_integration_id") REFERENCES "public"."server_integrations"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_state" ADD CONSTRAINT "user_state_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "chat_groups" ADD CONSTRAINT "chat_groups_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "chat_messages" ADD CONSTRAINT "chat_messages_session_id_chat_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."chat_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "chat_sessions" ADD CONSTRAINT "chat_sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "dashboard_defaults" ADD CONSTRAINT "dashboard_defaults_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "dashboards" ADD CONSTRAINT "dashboards_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "screens" ADD CONSTRAINT "screens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "workspaces" ADD CONSTRAINT "workspaces_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "extensions" ADD CONSTRAINT "extensions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "processes" ADD CONSTRAINT "processes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "skills" ADD CONSTRAINT "skills_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
@@ -316,6 +343,7 @@ ALTER TABLE "tools" ADD CONSTRAINT "tools_user_id_users_id_fk" FOREIGN KEY ("use
ALTER TABLE "queue_jobs" ADD CONSTRAINT "queue_jobs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "task_logs" ADD CONSTRAINT "task_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "terminal_containers" ADD CONSTRAINT "terminal_containers_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "email_accounts" ADD CONSTRAINT "email_accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "idx_token_blacklist_expires" ON "token_blacklist" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "idx_chat_groups_user" ON "chat_groups" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_messages_session_order" ON "chat_messages" USING btree ("session_id","sort_order");--> statement-breakpoint
@@ -327,7 +355,6 @@ CREATE INDEX "idx_extensions_user" ON "extensions" USING btree ("user_id");--> s
CREATE INDEX "idx_item_chats_type_item" ON "item_chats" USING btree ("item_type","item_id");--> statement-breakpoint
CREATE INDEX "idx_processes_scope" ON "processes" USING btree ("scope");--> statement-breakpoint
CREATE INDEX "idx_processes_user" ON "processes" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_resources_scope" ON "resources" USING btree ("scope");--> statement-breakpoint
CREATE INDEX "idx_skills_scope" ON "skills" USING btree ("scope");--> statement-breakpoint
CREATE INDEX "idx_skills_user" ON "skills" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_tasks_scope" ON "tasks" USING btree ("scope");--> statement-breakpoint
@@ -1,4 +0,0 @@
ALTER TABLE "workspaces" RENAME TO "dashboards";--> statement-breakpoint
ALTER TABLE "dashboards" DROP CONSTRAINT "workspaces_user_id_users_id_fk";--> statement-breakpoint
ALTER TABLE "dashboards" ADD CONSTRAINT "dashboards_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "dashboards" RENAME CONSTRAINT "uq_workspaces_user_id" TO "uq_dashboards_user_id";
@@ -1,5 +1,5 @@
{
"id": "65f7442c-6226-471f-b6b3-d559e820ab61",
"id": "7187e43e-021e-4623-8c74-8e5e7bc73926",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
@@ -347,29 +347,18 @@
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"server_integration_id": {
"name": "server_integration_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"profile": {
"name": "profile",
"config": {
"name": "config",
"type": "jsonb",
"primaryKey": false,
"notNull": false
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
@@ -400,6 +389,19 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"user_integrations_server_integration_id_server_integrations_id_fk": {
"name": "user_integrations_server_integration_id_server_integrations_id_fk",
"tableFrom": "user_integrations",
"tableTo": "server_integrations",
"columnsFrom": [
"server_integration_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -929,6 +931,177 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.dashboard_defaults": {
"name": "dashboard_defaults",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"terminals": {
"name": "terminals",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"host_terminals": {
"name": "host_terminals",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"dashboard_defaults_user_id_users_id_fk": {
"name": "dashboard_defaults_user_id_users_id_fk",
"tableFrom": "dashboard_defaults",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"dashboard_defaults_user_id_unique": {
"name": "dashboard_defaults_user_id_unique",
"nullsNotDistinct": false,
"columns": [
"user_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.dashboards": {
"name": "dashboards",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"config": {
"name": "config",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"layout": {
"name": "layout",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[]'::jsonb"
},
"terminals": {
"name": "terminals",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[]'::jsonb"
},
"host_terminals": {
"name": "host_terminals",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[]'::jsonb"
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"dashboards_user_id_users_id_fk": {
"name": "dashboards_user_id_users_id_fk",
"tableFrom": "dashboards",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"uq_dashboards_user_id": {
"name": "uq_dashboards_user_id",
"nullsNotDistinct": false,
"columns": [
"user_id",
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.projects": {
"name": "projects",
"schema": "",
@@ -972,6 +1145,13 @@
"notNull": true,
"default": "'[]'::jsonb"
},
"host_terminals": {
"name": "host_terminals",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
@@ -1047,6 +1227,20 @@
"notNull": true,
"default": "'[]'::jsonb"
},
"terminals": {
"name": "terminals",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"host_terminals": {
"name": "host_terminals",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
@@ -1086,109 +1280,6 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspaces": {
"name": "workspaces",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"config": {
"name": "config",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"layout": {
"name": "layout",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[]'::jsonb"
},
"terminals": {
"name": "terminals",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[]'::jsonb"
},
"host_terminals": {
"name": "host_terminals",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[]'::jsonb"
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workspaces_user_id_users_id_fk": {
"name": "workspaces_user_id_users_id_fk",
"tableFrom": "workspaces",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"uq_workspaces_user_id": {
"name": "uq_workspaces_user_id",
"nullsNotDistinct": false,
"columns": [
"user_id",
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.extensions": {
"name": "extensions",
"schema": "",
@@ -1514,108 +1605,6 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.resources": {
"name": "resources",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": true
},
"dir_name": {
"name": "dir_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"config": {
"name": "config",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"idx_resources_scope": {
"name": "idx_resources_scope",
"columns": [
{
"expression": "scope",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"uq_resources_scope_dir": {
"name": "uq_resources_scope_dir",
"nullsNotDistinct": false,
"columns": [
"scope",
"dir_name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.skills": {
"name": "skills",
"schema": "",
@@ -2427,6 +2416,200 @@
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.server_integrations": {
"name": "server_integrations",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"config": {
"name": "config",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"server_integrations_provider_unique": {
"name": "server_integrations_provider_unique",
"nullsNotDistinct": false,
"columns": [
"provider"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.email_accounts": {
"name": "email_accounts",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"display_name": {
"name": "display_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"imap_host": {
"name": "imap_host",
"type": "text",
"primaryKey": false,
"notNull": true
},
"imap_port": {
"name": "imap_port",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"imap_secure": {
"name": "imap_secure",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"auth_type": {
"name": "auth_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"credentials": {
"name": "credentials",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'connected'"
},
"sync_meta": {
"name": "sync_meta",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"email_accounts_user_id_users_id_fk": {
"name": "email_accounts_user_id_users_id_fk",
"tableFrom": "email_accounts",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"uq_email_accounts_user_email": {
"name": "uq_email_accounts_user_email",
"nullsNotDistinct": false,
"columns": [
"user_id",
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
@@ -5,15 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1772121115932,
"tag": "0000_clammy_meteorite",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1772265600000,
"tag": "0001_rename_workspaces_to_dashboards",
"when": 1772795711164,
"tag": "0000_futuristic_ink",
"breakpoints": true
}
]
+12
View File
@@ -52,5 +52,17 @@ export {
getAllSyncedAccounts,
} from './queries/email-accounts';
export {
getAllDashboardState,
upsertDashboard,
deleteDashboard,
upsertScreen,
deleteScreen,
upsertProject,
deleteProject,
getDefaults,
upsertDefaults,
} from './queries/dashboards';
export { db } from './db';
export * as schema from './schema';
@@ -0,0 +1,219 @@
import { eq, and } from 'drizzle-orm';
import { db } from '../db';
import { dashboards, screens, projects, dashboardDefaults } from '../schema';
// ── Full state read ──
export async function getAllDashboardState(userId: number): Promise<Record<string, unknown>> {
const result: Record<string, unknown> = {};
const [dashRows, screenRows, projectRows, defaultsRow] = await Promise.all([
db.select().from(dashboards).where(eq(dashboards.userId, userId)),
db.select().from(screens).where(eq(screens.userId, userId)),
db.select().from(projects).where(eq(projects.userId, userId)),
db
.select()
.from(dashboardDefaults)
.where(eq(dashboardDefaults.userId, userId))
.then((rows) => rows[0]),
]);
// Dashboards → workspaces array + per-dashboard keys
if (dashRows.length > 0) {
const sorted = [...dashRows].sort((a, b) => a.sortOrder - b.sortOrder);
result['workspaces'] = sorted.map((d) => ({ id: d.id, name: d.name, ...(d.config as object) }));
for (const d of dashRows) {
result[`ws-layout-${d.id}`] = d.layout;
result[`ws-terminals-${d.id}`] = d.terminals;
result[`ws-host-terminals-${d.id}`] = d.hostTerminals;
}
}
// Defaults
if (defaultsRow) {
result['ws-terminals-default'] = defaultsRow.terminals;
result['ws-host-terminals-default'] = defaultsRow.hostTerminals;
}
// Screens
for (const s of screenRows) {
result[`screens/${s.name}`] = s.layout;
}
// Projects
const projectList = projectRows.map((p) => ({
...(p.meta as object),
id: p.slug,
cwd: `/Projects/${p.slug}`,
}));
result['projects'] = projectList;
for (const p of projectRows) {
result[`proj-layout-${p.slug}`] = p.layout;
result[`proj-terminals-${p.slug}`] = p.terminals;
result[`proj-host-terminals-${p.slug}`] = p.hostTerminals;
}
return result;
}
// ── Dashboard CRUD ──
type UpsertDashboardData = {
name?: string;
config?: unknown;
layout?: unknown;
terminals?: unknown;
hostTerminals?: unknown;
sortOrder?: number;
};
export async function upsertDashboard(userId: number, id: string, data: UpsertDashboardData): Promise<void> {
const now = new Date();
const existing = await db
.select()
.from(dashboards)
.where(and(eq(dashboards.userId, userId), eq(dashboards.id, id)))
.then((rows) => rows[0]);
if (existing) {
const set: Record<string, unknown> = { updatedAt: now };
if (data.name !== undefined) set.name = data.name;
if (data.config !== undefined) set.config = data.config;
if (data.layout !== undefined) set.layout = data.layout;
if (data.terminals !== undefined) set.terminals = data.terminals;
if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals;
if (data.sortOrder !== undefined) set.sortOrder = data.sortOrder;
await db.update(dashboards).set(set).where(eq(dashboards.id, id));
} else {
await db.insert(dashboards).values({
id,
userId,
name: data.name ?? id,
config: data.config ?? {},
layout: data.layout ?? [],
terminals: data.terminals ?? [],
hostTerminals: data.hostTerminals ?? [],
sortOrder: data.sortOrder ?? 0,
createdAt: now,
updatedAt: now,
});
}
}
export async function deleteDashboard(userId: number, id: string): Promise<void> {
await db.delete(dashboards).where(and(eq(dashboards.userId, userId), eq(dashboards.id, id)));
}
// ── Screen CRUD ──
type UpsertScreenData = {
layout?: unknown;
terminals?: unknown;
hostTerminals?: unknown;
};
export async function upsertScreen(userId: number, name: string, data: UpsertScreenData): Promise<void> {
const now = new Date();
const set: Record<string, unknown> = { updatedAt: now };
if (data.layout !== undefined) set.layout = data.layout;
if (data.terminals !== undefined) set.terminals = data.terminals;
if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals;
await db
.insert(screens)
.values({
userId,
name,
layout: (data.layout ?? []) as never,
terminals: (data.terminals ?? {}) as never,
hostTerminals: (data.hostTerminals ?? {}) as never,
updatedAt: now,
})
.onConflictDoUpdate({
target: [screens.userId, screens.name],
set,
});
}
export async function deleteScreen(userId: number, name: string): Promise<void> {
await db.delete(screens).where(and(eq(screens.userId, userId), eq(screens.name, name)));
}
// ── Project CRUD ──
type UpsertProjectData = {
meta?: unknown;
layout?: unknown;
terminals?: unknown;
hostTerminals?: unknown;
};
export async function upsertProject(userId: number, slug: string, data: UpsertProjectData): Promise<void> {
const now = new Date();
const existing = await db
.select()
.from(projects)
.where(and(eq(projects.userId, userId), eq(projects.slug, slug)))
.then((rows) => rows[0]);
if (existing) {
const set: Record<string, unknown> = { updatedAt: now };
if (data.meta !== undefined) set.meta = data.meta;
if (data.layout !== undefined) set.layout = data.layout;
if (data.terminals !== undefined) set.terminals = data.terminals;
if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals;
await db
.update(projects)
.set(set)
.where(and(eq(projects.userId, userId), eq(projects.slug, slug)));
} else {
await db.insert(projects).values({
userId,
slug,
meta: (data.meta ?? {}) as never,
layout: (data.layout ?? []) as never,
terminals: (data.terminals ?? []) as never,
hostTerminals: (data.hostTerminals ?? {}) as never,
createdAt: now,
updatedAt: now,
});
}
}
export async function deleteProject(userId: number, slug: string): Promise<void> {
await db.delete(projects).where(and(eq(projects.userId, userId), eq(projects.slug, slug)));
}
// ── Defaults ──
type UpsertDefaultsData = {
terminals?: unknown;
hostTerminals?: unknown;
};
export async function getDefaults(userId: number): Promise<{ terminals: unknown; hostTerminals: unknown }> {
const [row] = await db.select().from(dashboardDefaults).where(eq(dashboardDefaults.userId, userId));
return { terminals: row?.terminals ?? {}, hostTerminals: row?.hostTerminals ?? {} };
}
export async function upsertDefaults(userId: number, data: UpsertDefaultsData): Promise<void> {
const now = new Date();
const set: Record<string, unknown> = { updatedAt: now };
if (data.terminals !== undefined) set.terminals = data.terminals;
if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals;
await db
.insert(dashboardDefaults)
.values({
userId,
terminals: (data.terminals ?? {}) as never,
hostTerminals: (data.hostTerminals ?? {}) as never,
updatedAt: now,
})
.onConflictDoUpdate({
target: dashboardDefaults.userId,
set,
});
}
@@ -1,40 +1,66 @@
import { pgTable, serial, text, integer, timestamp, jsonb, unique } from 'drizzle-orm/pg-core';
import { users } from './auth';
export const dashboards = pgTable('dashboards', {
id: text('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
config: jsonb('config').notNull().default({}),
layout: jsonb('layout').notNull().default([]),
terminals: jsonb('terminals').notNull().default([]),
hostTerminals: jsonb('host_terminals').notNull().default([]),
sortOrder: integer('sort_order').notNull().default(0),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_dashboards_user_id').on(table.userId, table.id),
]);
export const dashboards = pgTable(
'dashboards',
{
id: text('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
config: jsonb('config').notNull().default({}),
layout: jsonb('layout').notNull().default([]),
terminals: jsonb('terminals').notNull().default([]),
hostTerminals: jsonb('host_terminals').notNull().default([]),
sortOrder: integer('sort_order').notNull().default(0),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(table) => [unique('uq_dashboards_user_id').on(table.userId, table.id)],
);
export const screens = pgTable('screens', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
layout: jsonb('layout').notNull().default([]),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_screens_user_name').on(table.userId, table.name),
]);
export const screens = pgTable(
'screens',
{
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
layout: jsonb('layout').notNull().default([]),
terminals: jsonb('terminals').notNull().default({}),
hostTerminals: jsonb('host_terminals').notNull().default({}),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(table) => [unique('uq_screens_user_name').on(table.userId, table.name)],
);
export const projects = pgTable('projects', {
export const projects = pgTable(
'projects',
{
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
slug: text('slug').notNull(),
meta: jsonb('meta').notNull().default({}),
layout: jsonb('layout').notNull().default([]),
terminals: jsonb('terminals').notNull().default([]),
hostTerminals: jsonb('host_terminals').notNull().default({}),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(table) => [unique('uq_projects_user_slug').on(table.userId, table.slug)],
);
export const dashboardDefaults = pgTable('dashboard_defaults', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
slug: text('slug').notNull(),
meta: jsonb('meta').notNull().default({}),
layout: jsonb('layout').notNull().default([]),
terminals: jsonb('terminals').notNull().default([]),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' })
.unique(),
terminals: jsonb('terminals').notNull().default({}),
hostTerminals: jsonb('host_terminals').notNull().default({}),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => [
unique('uq_projects_user_slug').on(table.userId, table.slug),
]);
});