diff --git a/drizzle.config.ts b/drizzle.config.ts index 3a49452..c0b26f2 100755 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ "./src/db/schema-ai-config.ts", "./src/db/schema-theme.ts", "./src/db/schema-google.ts", + "./src/db/schema-dashboards.ts", ], out: "./drizzle", dialect: "sqlite", diff --git a/drizzle/0018_left_veda.sql b/drizzle/0018_left_veda.sql new file mode 100755 index 0000000..9bea5e7 --- /dev/null +++ b/drizzle/0018_left_veda.sql @@ -0,0 +1,12 @@ +CREATE TABLE `custom_dashboards` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `name` text NOT NULL, + `description` text DEFAULT '' NOT NULL, + `spec_data` text NOT NULL, + `queries` text NOT NULL, + `render_prompt` text NOT NULL, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/drizzle/meta/0018_snapshot.json b/drizzle/meta/0018_snapshot.json new file mode 100755 index 0000000..118f287 --- /dev/null +++ b/drizzle/meta/0018_snapshot.json @@ -0,0 +1,3604 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "106023e6-98ca-4849-82cf-63573d3e2012", + "prevId": "3ad6fba8-c3c6-44bd-bf44-fa3e8f997794", + "tables": { + "agent_conversations": { + "name": "agent_conversations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_conversations_user_id_users_id_fk": { + "name": "agent_conversations_user_id_users_id_fk", + "tableFrom": "agent_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_memories": { + "name": "agent_memories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "embedding": { + "name": "embedding", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_memories_conversation_id_agent_conversations_id_fk": { + "name": "agent_memories_conversation_id_agent_conversations_id_fk", + "tableFrom": "agent_memories", + "tableTo": "agent_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memories_user_id_users_id_fk": { + "name": "agent_memories_user_id_users_id_fk", + "tableFrom": "agent_memories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customers": { + "name": "customers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feedback": { + "name": "feedback", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_url": { + "name": "page_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewport_width": { + "name": "viewport_width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "viewport_height": { + "name": "viewport_height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_issue_url": { + "name": "github_issue_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feedback_interviews": { + "name": "feedback_interviews", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_role": { + "name": "user_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "responses": { + "name": "responses", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pain_points": { + "name": "pain_points", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feature_requests": { + "name": "feature_requests", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overall_sentiment": { + "name": "overall_sentiment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "github_issue_url": { + "name": "github_issue_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "feedback_interviews_user_id_users_id_fk": { + "name": "feedback_interviews_user_id_users_id_fk", + "tableFrom": "feedback_interviews", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group_members": { + "name": "group_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_members_group_id_groups_id_fk": { + "name": "group_members_group_id_groups_id_fk", + "tableFrom": "group_members", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_members_user_id_users_id_fk": { + "name": "group_members_user_id_users_id_fk", + "tableFrom": "group_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groups": { + "name": "groups", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groups_organization_id_organizations_id_fk": { + "name": "groups_organization_id_organizations_id_fk", + "tableFrom": "groups", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization_members": { + "name": "organization_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_members": { + "name": "project_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_members_project_id_projects_id_fk": { + "name": "project_members_project_id_projects_id_fk", + "tableFrom": "project_members", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_members_user_id_users_id_fk": { + "name": "project_members_user_id_users_id_fk", + "tableFrom": "project_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'OPEN'" + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_manager": { + "name": "project_manager", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "netsuite_job_id": { + "name": "netsuite_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organizations_id_fk": { + "name": "projects_organization_id_organizations_id_fk", + "tableFrom": "projects", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "push_tokens": { + "name": "push_tokens", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "push_tokens_user_id_users_id_fk": { + "name": "push_tokens_user_id_users_id_fk", + "tableFrom": "push_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_baselines": { + "name": "schedule_baselines", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_data": { + "name": "snapshot_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_baselines_project_id_projects_id_fk": { + "name": "schedule_baselines_project_id_projects_id_fk", + "tableFrom": "schedule_baselines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_tasks": { + "name": "schedule_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workdays": { + "name": "workdays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date_calculated": { + "name": "end_date_calculated", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'PENDING'" + }, + "is_critical_path": { + "name": "is_critical_path", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_milestone": { + "name": "is_milestone", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "percent_complete": { + "name": "percent_complete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "assigned_to": { + "name": "assigned_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_tasks_project_id_projects_id_fk": { + "name": "schedule_tasks_project_id_projects_id_fk", + "tableFrom": "schedule_tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "slab_memories": { + "name": "slab_memories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_type": { + "name": "memory_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "importance": { + "name": "importance", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0.7 + }, + "pinned": { + "name": "pinned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "slab_memories_user_id_users_id_fk": { + "name": "slab_memories_user_id_users_id_fk", + "tableFrom": "slab_memories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_dependencies": { + "name": "task_dependencies", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "predecessor_id": { + "name": "predecessor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "successor_id": { + "name": "successor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'FS'" + }, + "lag_days": { + "name": "lag_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "task_dependencies_predecessor_id_schedule_tasks_id_fk": { + "name": "task_dependencies_predecessor_id_schedule_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "schedule_tasks", + "columnsFrom": [ + "predecessor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_dependencies_successor_id_schedule_tasks_id_fk": { + "name": "task_dependencies_successor_id_schedule_tasks_id_fk", + "tableFrom": "task_dependencies", + "tableTo": "schedule_tasks", + "columnsFrom": [ + "successor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_members": { + "name": "team_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joined_at": { + "name": "joined_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "teams_organization_id_organizations_id_fk": { + "name": "teams_organization_id_organizations_id_fk", + "tableFrom": "teams", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'office'" + }, + "google_email": { + "name": "google_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_login_at": { + "name": "last_login_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendors": { + "name": "vendors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Subcontractor'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workday_exceptions": { + "name": "workday_exceptions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_date": { + "name": "start_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_date": { + "name": "end_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'non_working'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'company_holiday'" + }, + "recurrence": { + "name": "recurrence", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'one_time'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "workday_exceptions_project_id_projects_id_fk": { + "name": "workday_exceptions_project_id_projects_id_fk", + "tableFrom": "workday_exceptions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credit_memos": { + "name": "credit_memos", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memo_number": { + "name": "memo_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "issue_date": { + "name": "issue_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_applied": { + "name": "amount_applied", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_remaining": { + "name": "amount_remaining", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "credit_memos_customer_id_customers_id_fk": { + "name": "credit_memos_customer_id_customers_id_fk", + "tableFrom": "credit_memos", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credit_memos_project_id_projects_id_fk": { + "name": "credit_memos_project_id_projects_id_fk", + "tableFrom": "credit_memos", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_number": { + "name": "invoice_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "issue_date": { + "name": "issue_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subtotal": { + "name": "subtotal", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "tax": { + "name": "tax", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_paid": { + "name": "amount_paid", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_due": { + "name": "amount_due", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invoices_customer_id_customers_id_fk": { + "name": "invoices_customer_id_customers_id_fk", + "tableFrom": "invoices", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invoices_project_id_projects_id_fk": { + "name": "invoices_project_id_projects_id_fk", + "tableFrom": "invoices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "netsuite_auth": { + "name": "netsuite_auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_in": { + "name": "expires_in", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "netsuite_sync_log": { + "name": "netsuite_sync_log", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sync_type": { + "name": "sync_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_type": { + "name": "record_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "records_processed": { + "name": "records_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "records_failed": { + "name": "records_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "error_summary": { + "name": "error_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "netsuite_sync_metadata": { + "name": "netsuite_sync_metadata", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "local_table": { + "name": "local_table", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_record_id": { + "name": "local_record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "netsuite_record_type": { + "name": "netsuite_record_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "netsuite_internal_id": { + "name": "netsuite_internal_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_modified_local": { + "name": "last_modified_local", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_modified_remote": { + "name": "last_modified_remote", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'synced'" + }, + "conflict_data": { + "name": "conflict_data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "payments": { + "name": "payments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_type": { + "name": "payment_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payment_date": { + "name": "payment_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_number": { + "name": "reference_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "payments_customer_id_customers_id_fk": { + "name": "payments_customer_id_customers_id_fk", + "tableFrom": "payments", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payments_vendor_id_vendors_id_fk": { + "name": "payments_vendor_id_vendors_id_fk", + "tableFrom": "payments", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payments_project_id_projects_id_fk": { + "name": "payments_project_id_projects_id_fk", + "tableFrom": "payments", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vendor_bills": { + "name": "vendor_bills", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "netsuite_id": { + "name": "netsuite_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bill_number": { + "name": "bill_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "bill_date": { + "name": "bill_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subtotal": { + "name": "subtotal", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "tax": { + "name": "tax", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_paid": { + "name": "amount_paid", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "amount_due": { + "name": "amount_due", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "line_items": { + "name": "line_items", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "vendor_bills_vendor_id_vendors_id_fk": { + "name": "vendor_bills_vendor_id_vendors_id_fk", + "tableFrom": "vendor_bills", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendor_bills_project_id_projects_id_fk": { + "name": "vendor_bills_project_id_projects_id_fk", + "tableFrom": "vendor_bills", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_config": { + "name": "plugin_config", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_encrypted": { + "name": "is_encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_events": { + "name": "plugin_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "plugin_events_plugin_id_plugins_id_fk": { + "name": "plugin_events_plugin_id_plugins_id_fk", + "tableFrom": "plugin_events", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_events_user_id_users_id_fk": { + "name": "plugin_events_user_id_users_id_fk", + "tableFrom": "plugin_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "required_env_vars": { + "name": "required_env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled_by": { + "name": "enabled_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled_at": { + "name": "enabled_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "plugins_enabled_by_users_id_fk": { + "name": "plugins_enabled_by_users_id_fk", + "tableFrom": "plugins", + "tableTo": "users", + "columnsFrom": [ + "enabled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_items": { + "name": "agent_items", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "done": { + "name": "done", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_items_user_id_users_id_fk": { + "name": "agent_items_user_id_users_id_fk", + "tableFrom": "agent_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_config": { + "name": "agent_config", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_name": { + "name": "model_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt_cost": { + "name": "prompt_cost", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completion_cost": { + "name": "completion_cost", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "max_cost_per_million": { + "name": "max_cost_per_million", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_user_selection": { + "name": "allow_user_selection", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_config_updated_by_users_id_fk": { + "name": "agent_config_updated_by_users_id_fk", + "tableFrom": "agent_config", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agent_usage": { + "name": "agent_usage", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt_tokens": { + "name": "prompt_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "completion_tokens": { + "name": "completion_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_usage_conversation_id_agent_conversations_id_fk": { + "name": "agent_usage_conversation_id_agent_conversations_id_fk", + "tableFrom": "agent_usage", + "tableTo": "agent_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_usage_user_id_users_id_fk": { + "name": "agent_usage_user_id_users_id_fk", + "tableFrom": "agent_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_model_preference": { + "name": "user_model_preference", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prompt_cost": { + "name": "prompt_cost", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "completion_cost": { + "name": "completion_cost", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_model_preference_user_id_users_id_fk": { + "name": "user_model_preference_user_id_users_id_fk", + "tableFrom": "user_model_preference", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "custom_themes": { + "name": "custom_themes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "theme_data": { + "name": "theme_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "custom_themes_user_id_users_id_fk": { + "name": "custom_themes_user_id_users_id_fk", + "tableFrom": "custom_themes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_theme_preference": { + "name": "user_theme_preference", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "active_theme_id": { + "name": "active_theme_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_theme_preference_user_id_users_id_fk": { + "name": "user_theme_preference_user_id_users_id_fk", + "tableFrom": "user_theme_preference", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_auth": { + "name": "google_auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_account_key_encrypted": { + "name": "service_account_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_domain": { + "name": "workspace_domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared_drive_id": { + "name": "shared_drive_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared_drive_name": { + "name": "shared_drive_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connected_by": { + "name": "connected_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "google_auth_organization_id_organizations_id_fk": { + "name": "google_auth_organization_id_organizations_id_fk", + "tableFrom": "google_auth", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "google_auth_connected_by_users_id_fk": { + "name": "google_auth_connected_by_users_id_fk", + "tableFrom": "google_auth", + "tableTo": "users", + "columnsFrom": [ + "connected_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "google_starred_files": { + "name": "google_starred_files", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "google_file_id": { + "name": "google_file_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "google_starred_files_user_id_users_id_fk": { + "name": "google_starred_files_user_id_users_id_fk", + "tableFrom": "google_starred_files", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "custom_dashboards": { + "name": "custom_dashboards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "spec_data": { + "name": "spec_data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "queries": { + "name": "queries", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "render_prompt": { + "name": "render_prompt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "custom_dashboards_user_id_users_id_fk": { + "name": "custom_dashboards_user_id_users_id_fk", + "tableFrom": "custom_dashboards", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index efa4f2b..028a414 100755 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1770442889458, "tag": "0017_outstanding_colonel_america", "breakpoints": true + }, + { + "idx": 18, + "version": "6", + "when": 1770471997491, + "tag": "0018_left_veda", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/actions/dashboards.ts b/src/app/actions/dashboards.ts new file mode 100755 index 0000000..fdfdd56 --- /dev/null +++ b/src/app/actions/dashboards.ts @@ -0,0 +1,336 @@ +"use server" + +import { eq, and, desc } from "drizzle-orm" +import { getCloudflareContext } from "@opennextjs/cloudflare" +import { getDb } from "@/db" +import { customDashboards } from "@/db/schema-dashboards" +import { getCurrentUser } from "@/lib/auth" +import { revalidatePath } from "next/cache" + +const MAX_DASHBOARDS = 5 + +interface SavedQuery { + readonly key: string + readonly queryType: string + readonly id?: string + readonly search?: string + readonly limit?: number +} + +export async function getCustomDashboards(): Promise< + | { + readonly success: true + readonly data: ReadonlyArray<{ + readonly id: string + readonly name: string + readonly description: string + readonly updatedAt: string + }> + } + | { readonly success: false; readonly error: string } +> { + const user = await getCurrentUser() + if (!user) return { success: false, error: "not authenticated" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + const dashboards = await db.query.customDashboards.findMany({ + where: (d, { eq: e }) => e(d.userId, user.id), + orderBy: (d) => desc(d.updatedAt), + columns: { + id: true, + name: true, + description: true, + updatedAt: true, + }, + }) + + return { success: true, data: dashboards } +} + +export async function getCustomDashboardById( + dashboardId: string, +): Promise< + | { + readonly success: true + readonly data: { + readonly id: string + readonly name: string + readonly description: string + readonly specData: string + readonly queries: string + readonly renderPrompt: string + readonly updatedAt: string + } + } + | { readonly success: false; readonly error: string } +> { + const user = await getCurrentUser() + if (!user) return { success: false, error: "not authenticated" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + const dashboard = await db.query.customDashboards.findFirst({ + where: (d, { eq: e, and: a }) => + a(e(d.id, dashboardId), e(d.userId, user.id)), + }) + + if (!dashboard) { + return { success: false, error: "dashboard not found" } + } + + return { + success: true, + data: { + id: dashboard.id, + name: dashboard.name, + description: dashboard.description, + specData: dashboard.specData, + queries: dashboard.queries, + renderPrompt: dashboard.renderPrompt, + updatedAt: dashboard.updatedAt, + }, + } +} + +export async function saveCustomDashboard( + name: string, + description: string, + specData: string, + queries: string, + renderPrompt: string, + existingId?: string, +): Promise< + | { readonly success: true; readonly id: string } + | { readonly success: false; readonly error: string } +> { + const user = await getCurrentUser() + if (!user) return { success: false, error: "not authenticated" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + const now = new Date().toISOString() + const id = existingId ?? crypto.randomUUID() + + if (existingId) { + const existing = await db.query.customDashboards.findFirst({ + where: (d, { eq: e, and: a }) => + a(e(d.id, existingId), e(d.userId, user.id)), + }) + if (!existing) { + return { success: false, error: "dashboard not found" } + } + await db + .update(customDashboards) + .set({ + name, + description, + specData, + queries, + renderPrompt, + updatedAt: now, + }) + .where(eq(customDashboards.id, existingId)) + } else { + const count = await db.query.customDashboards.findMany({ + where: (d, { eq: e }) => e(d.userId, user.id), + columns: { id: true }, + }) + if (count.length >= MAX_DASHBOARDS) { + return { + success: false, + error: `Maximum of ${MAX_DASHBOARDS} dashboards reached. Delete one to create a new one.`, + } + } + await db.insert(customDashboards).values({ + id, + userId: user.id, + name, + description, + specData, + queries, + renderPrompt, + createdAt: now, + updatedAt: now, + }) + } + + revalidatePath("/", "layout") + return { success: true, id } +} + +export async function deleteCustomDashboard( + dashboardId: string, +): Promise< + | { readonly success: true } + | { readonly success: false; readonly error: string } +> { + const user = await getCurrentUser() + if (!user) return { success: false, error: "not authenticated" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + const existing = await db.query.customDashboards.findFirst({ + where: (d, { eq: e, and: a }) => + a(e(d.id, dashboardId), e(d.userId, user.id)), + }) + if (!existing) { + return { success: false, error: "dashboard not found" } + } + + await db + .delete(customDashboards) + .where( + and( + eq(customDashboards.id, dashboardId), + eq(customDashboards.userId, user.id), + ), + ) + + revalidatePath("/", "layout") + return { success: true } +} + +export async function executeDashboardQueries( + queriesJson: string, +): Promise< + | { + readonly success: true + readonly data: Record + } + | { readonly success: false; readonly error: string } +> { + const user = await getCurrentUser() + if (!user) return { success: false, error: "not authenticated" } + + const { env } = await getCloudflareContext() + const db = getDb(env.DB) + + let queries: ReadonlyArray + try { + queries = JSON.parse(queriesJson) as ReadonlyArray + } catch { + return { success: false, error: "invalid queries JSON" } + } + + const dataContext: Record = {} + + for (const q of queries) { + const cap = q.limit ?? 20 + try { + switch (q.queryType) { + case "customers": { + const rows = await db.query.customers.findMany({ + limit: cap, + ...(q.search + ? { + where: (c, { like }) => + like(c.name, `%${q.search}%`), + } + : {}), + }) + dataContext[q.key] = { data: rows, count: rows.length } + break + } + case "vendors": { + const rows = await db.query.vendors.findMany({ + limit: cap, + ...(q.search + ? { + where: (v, { like }) => + like(v.name, `%${q.search}%`), + } + : {}), + }) + dataContext[q.key] = { data: rows, count: rows.length } + break + } + case "projects": { + const rows = await db.query.projects.findMany({ + limit: cap, + ...(q.search + ? { + where: (p, { like }) => + like(p.name, `%${q.search}%`), + } + : {}), + }) + dataContext[q.key] = { data: rows, count: rows.length } + break + } + case "invoices": { + const rows = await db.query.invoices.findMany({ + limit: cap, + }) + dataContext[q.key] = { data: rows, count: rows.length } + break + } + case "vendor_bills": { + const rows = await db.query.vendorBills.findMany({ + limit: cap, + }) + dataContext[q.key] = { data: rows, count: rows.length } + break + } + case "schedule_tasks": { + const rows = await db.query.scheduleTasks.findMany({ + limit: cap, + ...(q.search + ? { + where: (t, { like }) => + like(t.title, `%${q.search}%`), + } + : {}), + }) + dataContext[q.key] = { data: rows, count: rows.length } + break + } + case "project_detail": { + if (q.id) { + const row = await db.query.projects.findFirst({ + where: (p, { eq: e }) => e(p.id, q.id!), + }) + dataContext[q.key] = row + ? { data: row } + : { error: "not found" } + } + break + } + case "customer_detail": { + if (q.id) { + const row = await db.query.customers.findFirst({ + where: (c, { eq: e }) => e(c.id, q.id!), + }) + dataContext[q.key] = row + ? { data: row } + : { error: "not found" } + } + break + } + case "vendor_detail": { + if (q.id) { + const row = await db.query.vendors.findFirst({ + where: (v, { eq: e }) => e(v.id, q.id!), + }) + dataContext[q.key] = row + ? { data: row } + : { error: "not found" } + } + break + } + default: + dataContext[q.key] = { error: "unknown query type" } + } + } catch (err) { + dataContext[q.key] = { + error: err instanceof Error ? err.message : "query failed", + } + } + } + + return { success: true, data: dataContext } +} diff --git a/src/app/api/agent/route.ts b/src/app/api/agent/route.ts index 0be0785..406a9e7 100755 --- a/src/app/api/agent/route.ts +++ b/src/app/api/agent/route.ts @@ -44,10 +44,16 @@ export async function POST(req: Request): Promise { ) } - const [memories, registry] = await Promise.all([ - loadMemoriesForPrompt(db, user.id), - getRegistry(db, envRecord), - ]) + const { getCustomDashboards } = await import( + "@/app/actions/dashboards" + ) + + const [memories, registry, dashboardResult] = + await Promise.all([ + loadMemoriesForPrompt(db, user.id), + getRegistry(db, envRecord), + getCustomDashboards(), + ]) const pluginSections = registry.getPromptSections() const pluginTools = registry.getTools() @@ -84,6 +90,9 @@ export async function POST(req: Request): Promise { timezone, memories, pluginSections, + dashboards: dashboardResult.success + ? dashboardResult.data + : [], mode: "full", }), messages: await convertToModelMessages( diff --git a/src/app/dashboard/boards/[id]/page.tsx b/src/app/dashboard/boards/[id]/page.tsx new file mode 100755 index 0000000..2851931 --- /dev/null +++ b/src/app/dashboard/boards/[id]/page.tsx @@ -0,0 +1,44 @@ +import { notFound } from "next/navigation" +import { + getCustomDashboardById, + executeDashboardQueries, +} from "@/app/actions/dashboards" +import { SavedDashboardView } from "@/components/saved-dashboard-view" + +interface Props { + readonly params: Promise<{ readonly id: string }> +} + +export default async function SavedDashboardPage({ + params, +}: Props) { + const { id } = await params + + const result = await getCustomDashboardById(id) + if (!result.success) notFound() + + const dashboard = result.data + const spec = JSON.parse(dashboard.specData) + + let dataContext: Record = {} + if (dashboard.queries) { + const queryResult = await executeDashboardQueries( + dashboard.queries, + ) + if (queryResult.success) { + dataContext = queryResult.data + } + } + + return ( + + ) +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index bc72b42..d56d8b4 100755 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -17,6 +17,7 @@ import { SidebarProvider, } from "@/components/ui/sidebar" import { getProjects } from "@/app/actions/projects" +import { getCustomDashboards } from "@/app/actions/dashboards" import { ProjectListProvider } from "@/components/project-list-provider" import { getCurrentUser, toSidebarUser } from "@/lib/auth" import { BiometricGuard } from "@/components/native/biometric-guard" @@ -29,11 +30,16 @@ export default async function DashboardLayout({ }: { readonly children: React.ReactNode }) { - const [projectList, authUser] = await Promise.all([ - getProjects(), - getCurrentUser(), - ]) + const [projectList, authUser, dashboardResult] = + await Promise.all([ + getProjects(), + getCurrentUser(), + getCustomDashboards(), + ]) const user = authUser ? toSidebarUser(authUser) : null + const dashboardList = dashboardResult.success + ? dashboardResult.data + : [] return ( @@ -51,7 +57,7 @@ export default async function DashboardLayout({ } as React.CSSProperties } > - + diff --git a/src/components/agent/chat-provider.tsx b/src/components/agent/chat-provider.tsx index 49ccf93..7556753 100755 --- a/src/components/agent/chat-provider.tsx +++ b/src/components/agent/chat-provider.tsx @@ -78,6 +78,10 @@ interface RenderContextValue { data: Record ) => void clearRender: () => void + loadSpec: ( + spec: Spec, + data: Record + ) => void } const RenderContext = @@ -171,6 +175,8 @@ export function ChatProvider({ const [dataContext, setDataContext] = React.useState< Record >({}) + const [loadedSpec, setLoadedSpec] = + React.useState(null) const router = useRouter() const pathname = usePathname() @@ -228,13 +234,19 @@ export function ChatProvider({ const routerRef = React.useRef(router) routerRef.current = router + const loadedSpecRef = React.useRef(loadedSpec) + loadedSpecRef.current = loadedSpec + const triggerRender = React.useCallback( (prompt: string, data: Record) => { setDataContext(data) + setLoadedSpec(null) renderSendRef.current(prompt, { dataContext: data, previousSpec: - renderSpecRef.current ?? undefined, + renderSpecRef.current ?? + loadedSpecRef.current ?? + undefined, }) }, [] @@ -243,8 +255,18 @@ export function ChatProvider({ const clearRender = React.useCallback(() => { renderClearRef.current() setDataContext({}) + setLoadedSpec(null) }, []) + const loadSpec = React.useCallback( + (spec: Spec, data: Record) => { + renderClearRef.current() + setLoadedSpec(spec) + setDataContext(data) + }, + [], + ) + // watch chat messages for generateUI tool results // and trigger render stream directly (no event chain) const renderDispatchedRef = React.useRef( @@ -275,6 +297,118 @@ export function ChatProvider({ triggerRender(result.renderPrompt, result.dataContext) }, [chat.messages, triggerRender]) + // listen for save-dashboard events from tool dispatch + React.useEffect(() => { + const handler = async (e: Event) => { + const detail = (e as CustomEvent).detail as { + name?: string + description?: string + dashboardId?: string + } + if (!detail?.name) return + + const currentSpec = + renderSpecRef.current ?? loadedSpecRef.current + if (!currentSpec) return + + const { saveCustomDashboard } = await import( + "@/app/actions/dashboards" + ) + + const result = await saveCustomDashboard( + detail.name, + detail.description ?? "", + JSON.stringify(currentSpec), + JSON.stringify([]), + detail.name, + detail.dashboardId, + ) + + if (result.success) { + window.dispatchEvent( + new CustomEvent("agent-toast", { + detail: { + message: `Dashboard "${detail.name}" saved`, + type: "success", + }, + }) + ) + } else { + window.dispatchEvent( + new CustomEvent("agent-toast", { + detail: { + message: result.error, + type: "error", + }, + }) + ) + } + } + + window.addEventListener( + "agent-save-dashboard", + handler + ) + return () => + window.removeEventListener( + "agent-save-dashboard", + handler + ) + }, []) + + // listen for load-dashboard events from tool dispatch + React.useEffect(() => { + const handler = async (e: Event) => { + const detail = (e as CustomEvent).detail as { + dashboardId?: string + spec?: Spec + queries?: string + renderPrompt?: string + editPrompt?: string + } + if (!detail?.spec) return + + // run saved queries for fresh data + let freshData: Record = {} + if (detail.queries) { + const { executeDashboardQueries } = await import( + "@/app/actions/dashboards" + ) + const result = await executeDashboardQueries( + detail.queries, + ) + if (result.success) { + freshData = result.data + } + } + + loadSpec(detail.spec, freshData) + + // navigate to /dashboard + if (pathnameRef.current !== "/dashboard") { + routerRef.current.push("/dashboard") + } + setIsOpen(true) + + // if editPrompt provided, trigger re-render + if (detail.editPrompt) { + setTimeout(() => { + triggerRender(detail.editPrompt!, freshData) + }, 100) + } + } + + window.addEventListener( + "agent-load-dashboard", + handler + ) + return () => + window.removeEventListener( + "agent-load-dashboard", + handler + ) + }, [loadSpec, triggerRender]) + // listen for navigation events from rendered UI React.useEffect(() => { const handler = (e: Event) => { @@ -347,6 +481,7 @@ export function ChatProvider({ setConversationId(crypto.randomUUID()) setResumeLoaded(true) clearRender() + setLoadedSpec(null) renderDispatchedRef.current.clear() }, [chat.setMessages, clearRender]) @@ -389,20 +524,23 @@ export function ChatProvider({ const renderValue = React.useMemo( () => ({ - spec: renderStream.spec, + spec: renderStream.spec ?? loadedSpec, isRendering: renderStream.isStreaming, error: renderStream.error, dataContext, triggerRender, clearRender, + loadSpec, }), [ renderStream.spec, + loadedSpec, renderStream.isStreaming, renderStream.error, dataContext, triggerRender, clearRender, + loadSpec, ] ) diff --git a/src/components/agent/rendered-view.tsx b/src/components/agent/rendered-view.tsx index b8d83fd..71710e6 100755 --- a/src/components/agent/rendered-view.tsx +++ b/src/components/agent/rendered-view.tsx @@ -1,9 +1,24 @@ "use client" -import { XIcon, Loader2Icon } from "lucide-react" +import * as React from "react" +import { + XIcon, + Loader2Icon, + BookmarkIcon, +} from "lucide-react" import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" import { useRenderState } from "./chat-provider" import { CompassRenderer } from "@/lib/agent/render/compass-renderer" +import { saveCustomDashboard } from "@/app/actions/dashboards" export function RenderedView() { const { @@ -14,8 +29,46 @@ export function RenderedView() { clearRender, } = useRenderState() + const [saveOpen, setSaveOpen] = React.useState(false) + const [saveName, setSaveName] = React.useState("") + const [saving, setSaving] = React.useState(false) + const hasRoot = !!spec?.root + const handleSave = async () => { + if (!saveName.trim() || !spec) return + setSaving(true) + const result = await saveCustomDashboard( + saveName.trim(), + "", + JSON.stringify(spec), + JSON.stringify([]), + saveName.trim(), + ) + setSaving(false) + if (result.success) { + setSaveOpen(false) + setSaveName("") + window.dispatchEvent( + new CustomEvent("agent-toast", { + detail: { + message: `Dashboard "${saveName.trim()}" saved`, + type: "success", + }, + }) + ) + } else { + window.dispatchEvent( + new CustomEvent("agent-toast", { + detail: { + message: result.error, + type: "error", + }, + }) + ) + } + } + return (
{/* Header bar */} @@ -31,17 +84,68 @@ export function RenderedView() { Generated by Slab )}
- +
+ {hasRoot && !isRendering && ( + + )} + +
+ + + + Save as Dashboard + +
+
+ + + setSaveName(e.target.value) + } + onKeyDown={(e) => { + if (e.key === "Enter") handleSave() + }} + /> +
+
+ + + + +
+
+ {/* Rendered content */}
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index f807f45..2c51754 100755 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -18,6 +18,7 @@ import { import { usePathname } from "next/navigation" import { NavMain } from "@/components/nav-main" +import { NavDashboards } from "@/components/nav-dashboards" import { NavSecondary } from "@/components/nav-secondary" import { NavFiles } from "@/components/nav-files" import { NavProjects } from "@/components/nav-projects" @@ -96,8 +97,13 @@ const data = { function SidebarNav({ projects, + dashboards = [], }: { projects: { id: string; name: string }[] + dashboards?: ReadonlyArray<{ + readonly id: string + readonly name: string + }> }) { const pathname = usePathname() const { state, setOpen } = useSidebar() @@ -146,6 +152,7 @@ function SidebarNav({ {mode === "main" && ( <> + )} @@ -155,10 +162,12 @@ function SidebarNav({ export function AppSidebar({ projects = [], + dashboards = [], user, ...props }: React.ComponentProps & { readonly projects?: ReadonlyArray<{ readonly id: string; readonly name: string }> + readonly dashboards?: ReadonlyArray<{ readonly id: string; readonly name: string }> readonly user: SidebarUser | null }) { return ( @@ -192,7 +201,10 @@ export function AppSidebar({ - + diff --git a/src/components/nav-dashboards.tsx b/src/components/nav-dashboards.tsx new file mode 100755 index 0000000..ecdc62a --- /dev/null +++ b/src/components/nav-dashboards.tsx @@ -0,0 +1,49 @@ +"use client" + +import { IconLayoutDashboard } from "@tabler/icons-react" +import Link from "next/link" + +import { + SidebarGroup, + SidebarGroupLabel, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +interface Dashboard { + readonly id: string + readonly name: string +} + +export function NavDashboards({ + dashboards, +}: { + readonly dashboards: ReadonlyArray +}) { + if (dashboards.length === 0) return null + + return ( + + Dashboards + + + {dashboards.map((d) => ( + + + + + {d.name} + + + + ))} + + + + ) +} diff --git a/src/components/saved-dashboard-view.tsx b/src/components/saved-dashboard-view.tsx new file mode 100755 index 0000000..e84e04a --- /dev/null +++ b/src/components/saved-dashboard-view.tsx @@ -0,0 +1,117 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { + RefreshCwIcon, + Trash2Icon, + Loader2Icon, +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { CompassRenderer } from "@/lib/agent/render/compass-renderer" +import { + deleteCustomDashboard, + executeDashboardQueries, +} from "@/app/actions/dashboards" +import type { Spec } from "@json-render/react" + +interface SavedDashboardViewProps { + readonly dashboard: { + readonly id: string + readonly name: string + readonly description: string + } + readonly spec: Spec + readonly dataContext: Record +} + +export function SavedDashboardView({ + dashboard, + spec, + dataContext: initialData, +}: SavedDashboardViewProps) { + const router = useRouter() + const [refreshing, setRefreshing] = React.useState(false) + const [deleting, setDeleting] = React.useState(false) + const [data, setData] = + React.useState>(initialData) + + const handleRefresh = async () => { + setRefreshing(true) + const result = await executeDashboardQueries( + JSON.stringify([]), + ) + if (result.success) { + setData(result.data) + } + setRefreshing(false) + } + + const handleDelete = async () => { + if (!confirm("Delete this dashboard?")) return + setDeleting(true) + const result = await deleteCustomDashboard(dashboard.id) + if (result.success) { + router.push("/dashboard") + } else { + setDeleting(false) + window.dispatchEvent( + new CustomEvent("agent-toast", { + detail: { + message: result.error, + type: "error", + }, + }) + ) + } + } + + return ( +
+
+
+

+ {dashboard.name} +

+ {dashboard.description && ( +

+ {dashboard.description} +

+ )} +
+
+ + +
+
+ +
+
+ +
+
+
+ ) +} diff --git a/src/db/index.ts b/src/db/index.ts index 77d8cc0..1125b61 100755 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -6,6 +6,7 @@ import * as agentSchema from "./schema-agent" import * as aiConfigSchema from "./schema-ai-config" import * as themeSchema from "./schema-theme" import * as googleSchema from "./schema-google" +import * as dashboardSchema from "./schema-dashboards" const allSchemas = { ...schema, @@ -15,6 +16,7 @@ const allSchemas = { ...aiConfigSchema, ...themeSchema, ...googleSchema, + ...dashboardSchema, } export function getDb(d1: D1Database) { diff --git a/src/db/schema-dashboards.ts b/src/db/schema-dashboards.ts new file mode 100755 index 0000000..a3e4fe2 --- /dev/null +++ b/src/db/schema-dashboards.ts @@ -0,0 +1,24 @@ +import { sqliteTable, text } from "drizzle-orm/sqlite-core" +import { users } from "./schema" + +export const customDashboards = sqliteTable( + "custom_dashboards", + { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + description: text("description").notNull().default(""), + specData: text("spec_data").notNull(), + queries: text("queries").notNull(), + renderPrompt: text("render_prompt").notNull(), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull(), + }, +) + +export type CustomDashboard = + typeof customDashboards.$inferSelect +export type NewCustomDashboard = + typeof customDashboards.$inferInsert diff --git a/src/lib/agent/chat-adapter.ts b/src/lib/agent/chat-adapter.ts index 53736cb..d778840 100755 --- a/src/lib/agent/chat-adapter.ts +++ b/src/lib/agent/chat-adapter.ts @@ -134,6 +134,26 @@ export function initializeActionHandlers( } }) + registerActionHandler("SAVE_DASHBOARD", (payload) => { + if (typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent("agent-save-dashboard", { + detail: payload, + }) + ) + } + }) + + registerActionHandler("LOAD_DASHBOARD", (payload) => { + if (typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent("agent-load-dashboard", { + detail: payload, + }) + ) + } + }) + registerActionHandler("APPLY_THEME", (payload) => { if (typeof window !== "undefined") { window.dispatchEvent( @@ -163,6 +183,8 @@ export const ALL_HANDLER_TYPES = [ "SCROLL_TO", "FOCUS_ELEMENT", "GENERATE_UI", + "SAVE_DASHBOARD", + "LOAD_DASHBOARD", "APPLY_THEME", "PREVIEW_THEME", ] as const @@ -227,6 +249,28 @@ export function dispatchToolActions( }, }) break + case "save_dashboard": + executeAction({ + type: "SAVE_DASHBOARD", + payload: { + name: output.name, + description: output.description, + dashboardId: output.dashboardId, + }, + }) + break + case "load_dashboard": + executeAction({ + type: "LOAD_DASHBOARD", + payload: { + dashboardId: output.dashboardId, + spec: output.spec, + queries: output.queries, + renderPrompt: output.renderPrompt, + editPrompt: output.editPrompt, + }, + }) + break case "apply_theme": executeAction({ type: "APPLY_THEME", diff --git a/src/lib/agent/system-prompt.ts b/src/lib/agent/system-prompt.ts index 53f3b22..f0103df 100755 --- a/src/lib/agent/system-prompt.ts +++ b/src/lib/agent/system-prompt.ts @@ -21,6 +21,12 @@ interface ToolMeta { readonly adminOnly?: true } +interface DashboardSummary { + readonly id: string + readonly name: string + readonly description: string +} + interface PromptContext { readonly userName: string readonly userRole: string @@ -28,6 +34,7 @@ interface PromptContext { readonly memories?: string readonly timezone?: string readonly pluginSections?: ReadonlyArray + readonly dashboards?: ReadonlyArray readonly mode?: PromptMode } @@ -60,7 +67,8 @@ const TOOL_REGISTRY: ReadonlyArray = [ "/dashboard/projects/{id}/schedule, " + "/dashboard/customers, /dashboard/vendors, " + "/dashboard/financials, /dashboard/people, " + - "/dashboard/files. If the page doesn't exist, " + + "/dashboard/files, /dashboard/boards/{id}. " + + "If the page doesn't exist, " + "tell the user what's available.", category: "navigation", }, @@ -184,6 +192,36 @@ const TOOL_REGISTRY: ReadonlyArray = [ "(not presets).", category: "ui", }, + { + name: "saveDashboard", + summary: + "Save the current rendered UI as a named dashboard. " + + "The client captures the spec and data automatically. " + + "Pass dashboardId to update an existing dashboard.", + category: "ui", + }, + { + name: "listDashboards", + summary: + "List the user's saved custom dashboards with " + + "their IDs, names, and descriptions.", + category: "ui", + }, + { + name: "editDashboard", + summary: + "Load a saved dashboard for editing. Loads the spec " + + "into the render context on /dashboard. Optionally " + + "pass editPrompt to trigger immediate re-generation.", + category: "ui", + }, + { + name: "deleteDashboard", + summary: + "Delete a saved dashboard. Always confirm with " + + "the user before deleting.", + category: "ui", + }, ] // categories included in minimal mode @@ -306,6 +344,8 @@ function buildFirstInteraction( 'show you recent commits, PRs, issues, and contributor activity."', '"I can also conduct a quick UX interview if you\'d like ' + 'to share feedback about Compass."', + '"I can build you a custom dashboard with charts and ' + + 'stats — and save it so you can access it anytime."', ] return [ @@ -554,6 +594,56 @@ function buildThemingRules( ] } +function buildDashboardRules( + ctx: PromptContext, + mode: PromptMode, +): ReadonlyArray { + if (mode !== "full") return [] + + const lines = [ + "## Custom Dashboards", + "Users can save generated UIs as persistent dashboards " + + "that appear in the sidebar and can be revisited anytime.", + "", + "**Workflow:**", + "1. User asks for a dashboard (e.g. \"build me a " + + "project overview\")", + "2. Use queryData to fetch data, then generateUI to " + + "build the UI", + "3. Once the user is happy, use saveDashboard to persist it", + "4. The dashboard appears in the sidebar at " + + "/dashboard/boards/{id}", + "", + "**Editing:**", + "- Use editDashboard to load a saved dashboard for editing", + "- After loading, use generateUI to make changes " + + "(the system sends patches against the previous spec)", + "- Use saveDashboard with the dashboardId to save updates", + "", + "**Limits:**", + "- Maximum 5 dashboards per user", + "- If the user hits the limit, suggest deleting one first", + "", + "**When to offer dashboard saving:**", + "- After generating a useful UI the user seems happy with", + '- When the user says "save this" or "keep this"', + "- Don't push it — offer once after a good generation", + ] + + if (ctx.dashboards?.length) { + lines.push( + "", + "**User's saved dashboards:**", + ...ctx.dashboards.map( + (d) => `- ${d.name} (id: ${d.id})` + + (d.description ? `: ${d.description}` : ""), + ), + ) + } + + return lines +} + function buildGuidelines( mode: PromptMode, ): ReadonlyArray { @@ -622,6 +712,7 @@ export function buildSystemPrompt(ctx: PromptContext): string { buildInterviewProtocol(state.mode), buildGitHubGuidance(state.mode), buildThemingRules(state.mode), + buildDashboardRules(ctx, state.mode), buildGuidelines(state.mode), buildPluginSections(ctx.pluginSections, state.mode), ] diff --git a/src/lib/agent/tools.ts b/src/lib/agent/tools.ts index 68442be..95be84d 100755 --- a/src/lib/agent/tools.ts +++ b/src/lib/agent/tools.ts @@ -16,6 +16,11 @@ import { saveCustomTheme, setUserThemePreference, } from "@/app/actions/themes" +import { + getCustomDashboards, + getCustomDashboardById, + deleteCustomDashboard, +} from "@/app/actions/dashboards" import { THEME_PRESETS, findPreset } from "@/lib/theme/presets" import type { ThemeDefinition, ColorMap, ThemeFonts, ThemeTokens, ThemeShadows } from "@/lib/theme/types" @@ -58,6 +63,7 @@ const VALID_ROUTES: ReadonlyArray = [ /^\/dashboard\/people$/, /^\/dashboard\/files$/, /^\/dashboard\/files\/.+$/, + /^\/dashboard\/boards\/[^/]+$/, ] function isValidRoute(path: string): boolean { @@ -275,7 +281,7 @@ export const agentTools = { "/dashboard/projects/{id}/schedule, " + "/dashboard/customers, /dashboard/vendors, " + "/dashboard/financials, /dashboard/people, " + - "/dashboard/files", + "/dashboard/files, /dashboard/boards/{id}", } } return { @@ -588,6 +594,103 @@ export const agentTools = { }, }), + saveDashboard: tool({ + description: + "Save the currently rendered UI as a named dashboard. " + + "The client captures the current spec and data context " + + "automatically. Returns an action for the client to " + + "handle the save.", + inputSchema: z.object({ + name: z.string().describe("Dashboard display name"), + description: z.string().optional().describe( + "Brief description of the dashboard", + ), + dashboardId: z.string().optional().describe( + "Existing dashboard ID to update (for edits)", + ), + }), + execute: async (input: { + name: string + description?: string + dashboardId?: string + }) => ({ + action: "save_dashboard" as const, + name: input.name, + description: input.description ?? "", + dashboardId: input.dashboardId, + }), + }), + + listDashboards: tool({ + description: + "List the user's saved custom dashboards.", + inputSchema: z.object({}), + execute: async () => { + const result = await getCustomDashboards() + if (!result.success) return { error: result.error } + return { + dashboards: result.data, + count: result.data.length, + } + }, + }), + + editDashboard: tool({ + description: + "Load a saved dashboard for editing. The client " + + "injects the spec into the render context and " + + "navigates to /dashboard. Optionally pass an " + + "editPrompt to trigger immediate re-generation.", + inputSchema: z.object({ + dashboardId: z.string().describe( + "ID of the dashboard to edit", + ), + editPrompt: z.string().optional().describe( + "Description of changes to make", + ), + }), + execute: async (input: { + dashboardId: string + editPrompt?: string + }) => { + const result = await getCustomDashboardById( + input.dashboardId, + ) + if (!result.success) return { error: result.error } + + return { + action: "load_dashboard" as const, + dashboardId: input.dashboardId, + spec: JSON.parse(result.data.specData), + queries: result.data.queries, + renderPrompt: result.data.renderPrompt, + editPrompt: input.editPrompt, + } + }, + }), + + deleteDashboard: tool({ + description: + "Delete a saved dashboard. Always confirm with " + + "the user before deleting.", + inputSchema: z.object({ + dashboardId: z.string().describe( + "ID of the dashboard to delete", + ), + }), + execute: async (input: { dashboardId: string }) => { + const result = await deleteCustomDashboard( + input.dashboardId, + ) + if (!result.success) return { error: result.error } + return { + action: "toast" as const, + message: "Dashboard deleted", + type: "success", + } + }, + }), + editTheme: tool({ description: "Edit an existing custom theme. Provide the theme ID " +