diff --git a/drizzle-pg/0012_dashboard.sql b/drizzle-pg/0012_dashboard.sql new file mode 100644 index 0000000..70321a1 --- /dev/null +++ b/drizzle-pg/0012_dashboard.sql @@ -0,0 +1,33 @@ +CREATE TABLE "backlink_snapshots" ( + "id" serial PRIMARY KEY NOT NULL, + "project_id" text NOT NULL, + "domain" text NOT NULL, + "rank" integer, + "backlinks" integer, + "referring_domains" integer, + "broken_backlinks" integer, + "new_backlinks" integer, + "lost_backlinks" integer, + "new_referring_domains" integer, + "lost_referring_domains" integer, + "captured_at" text DEFAULT to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL +); +--> statement-breakpoint +CREATE TABLE "organization_activation_state" ( + "organization_id" text PRIMARY KEY NOT NULL, + "first_mcp_authorized_at" text, + "first_mcp_tool_call_at" text, + "updated_at" text DEFAULT to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL +); +--> statement-breakpoint +CREATE TABLE "project_activation_state" ( + "project_id" text PRIMARY KEY NOT NULL, + "competitor_step_clicked_at" text, + "mcp_card_dismissed_at" text, + "updated_at" text DEFAULT to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL +); +--> statement-breakpoint +ALTER TABLE "backlink_snapshots" ADD CONSTRAINT "backlink_snapshots_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "organization_activation_state" ADD CONSTRAINT "organization_activation_state_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "project_activation_state" ADD CONSTRAINT "project_activation_state_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "backlink_snapshots_project_captured_idx" ON "backlink_snapshots" USING btree ("project_id","captured_at"); \ No newline at end of file diff --git a/drizzle-pg/meta/0012_snapshot.json b/drizzle-pg/meta/0012_snapshot.json new file mode 100644 index 0000000..1ac9400 --- /dev/null +++ b/drizzle-pg/meta/0012_snapshot.json @@ -0,0 +1,3708 @@ +{ + "id": "015b57f2-c025-43ae-a1fe-d1e97ff40029", + "prevId": "593e3077-49ea-4f90-aa78-81bd18b8b1b2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.backlink_snapshots": { + "name": "backlink_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backlinks": { + "name": "backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "referring_domains": { + "name": "referring_domains", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "broken_backlinks": { + "name": "broken_backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "new_backlinks": { + "name": "new_backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lost_backlinks": { + "name": "lost_backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "new_referring_domains": { + "name": "new_referring_domains", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lost_referring_domains": { + "name": "lost_referring_domains", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "captured_at": { + "name": "captured_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "backlink_snapshots_project_captured_idx": { + "name": "backlink_snapshots_project_captured_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "captured_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "backlink_snapshots_project_id_projects_id_fk": { + "name": "backlink_snapshots_project_id_projects_id_fk", + "tableFrom": "backlink_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.keyword_metrics": { + "name": "keyword_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "competition": { + "name": "competition", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monthly_searches": { + "name": "monthly_searches", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "keyword_metrics_unique_project_keyword_location_language": { + "name": "keyword_metrics_unique_project_keyword_location_language", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "language_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keyword_metrics_lookup_idx": { + "name": "keyword_metrics_lookup_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "language_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fetched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "keyword_metrics_project_id_projects_id_fk": { + "name": "keyword_metrics_project_id_projects_id_fk", + "tableFrom": "keyword_metrics", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_activation_state": { + "name": "organization_activation_state", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_mcp_authorized_at": { + "name": "first_mcp_authorized_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_mcp_tool_call_at": { + "name": "first_mcp_tool_call_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_activation_state_organization_id_organization_id_fk": { + "name": "organization_activation_state_organization_id_organization_id_fk", + "tableFrom": "organization_activation_state", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_activation_state": { + "name": "project_activation_state", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "competitor_step_clicked_at": { + "name": "competitor_step_clicked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_card_dismissed_at": { + "name": "mcp_card_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": {}, + "foreignKeys": { + "project_activation_state_project_id_projects_id_fk": { + "name": "project_activation_state_project_id_projects_id_fk", + "tableFrom": "project_activation_state", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "projects_one_default_per_organization_idx": { + "name": "projects_one_default_per_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"projects\".\"name\" = 'Default' AND \"projects\".\"domain\" IS NULL AND \"projects\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "projects_organization_id_idx": { + "name": "projects_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rank_check_runs": { + "name": "rank_check_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "keywords_total": { + "name": "keywords_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "keywords_checked": { + "name": "keywords_checked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_subset_run": { + "name": "is_subset_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "rank_check_runs_config_idx": { + "name": "rank_check_runs_config_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_check_runs_project_idx": { + "name": "rank_check_runs_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_check_runs_one_active_per_config_idx": { + "name": "rank_check_runs_one_active_per_config_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"rank_check_runs\".\"status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rank_check_runs_config_id_rank_tracking_configs_id_fk": { + "name": "rank_check_runs_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rank_check_runs_project_id_projects_id_fk": { + "name": "rank_check_runs_project_id_projects_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rank_snapshots": { + "name": "rank_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_keyword_id": { + "name": "tracking_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serp_features": { + "name": "serp_features", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "rank_snapshots_keyword_device_idx": { + "name": "rank_snapshots_keyword_device_idx", + "columns": [ + { + "expression": "tracking_keyword_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_snapshots_run_keyword_device_idx": { + "name": "rank_snapshots_run_keyword_device_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tracking_keyword_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rank_snapshots_run_id_rank_check_runs_id_fk": { + "name": "rank_snapshots_run_id_rank_check_runs_id_fk", + "tableFrom": "rank_snapshots", + "tableTo": "rank_check_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rank_tracking_configs": { + "name": "rank_tracking_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "devices": { + "name": "devices", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "serp_depth": { + "name": "serp_depth", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "schedule_interval": { + "name": "schedule_interval", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'weekly'" + }, + "location_name": { + "name": "location_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_skip_reason": { + "name": "last_skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "rank_tracking_configs_project_active_created_idx": { + "name": "rank_tracking_configs_project_active_created_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_tracking_configs_national_idx": { + "name": "rank_tracking_configs_national_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"rank_tracking_configs\".\"location_name\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "rank_tracking_configs_local_idx": { + "name": "rank_tracking_configs_local_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"rank_tracking_configs\".\"location_name\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rank_tracking_configs_project_id_projects_id_fk": { + "name": "rank_tracking_configs_project_id_projects_id_fk", + "tableFrom": "rank_tracking_configs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rank_tracking_keywords": { + "name": "rank_tracking_keywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "metrics_fetched_at": { + "name": "metrics_fetched_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "rank_tracking_keywords_config_keyword_idx": { + "name": "rank_tracking_keywords_config_keyword_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk": { + "name": "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_tracking_keywords", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_keyword_tag_assignments": { + "name": "saved_keyword_tag_assignments", + "schema": "", + "columns": { + "saved_keyword_id": { + "name": "saved_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "saved_keyword_tag_assignments_unique_idx": { + "name": "saved_keyword_tag_assignments_unique_idx", + "columns": [ + { + "expression": "saved_keyword_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saved_keyword_tag_assignments_tag_idx": { + "name": "saved_keyword_tag_assignments_tag_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk": { + "name": "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keywords", + "columnsFrom": [ + "saved_keyword_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk": { + "name": "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keyword_tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_keyword_tags": { + "name": "saved_keyword_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_name": { + "name": "normalized_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "saved_keyword_tags_project_normalized_name_idx": { + "name": "saved_keyword_tags_project_normalized_name_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saved_keyword_tags_project_name_idx": { + "name": "saved_keyword_tags_project_name_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saved_keyword_tags_project_id_projects_id_fk": { + "name": "saved_keyword_tags_project_id_projects_id_fk", + "tableFrom": "saved_keyword_tags", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_keywords": { + "name": "saved_keywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "saved_keywords_unique_project_keyword_location_language": { + "name": "saved_keywords_unique_project_keyword_location_language", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "keyword", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "location_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "language_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saved_keywords_project_created_idx": { + "name": "saved_keywords_project_created_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saved_keywords_project_id_projects_id_fk": { + "name": "saved_keywords_project_id_projects_id_fk", + "tableFrom": "saved_keywords", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_onboarding_answers": { + "name": "user_onboarding_answers", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interested_features": { + "name": "interested_features", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "work_for": { + "name": "work_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_website_count": { + "name": "client_website_count", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "found_via": { + "name": "found_via", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_setup_intent": { + "name": "mcp_setup_intent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gsc_nudge_dismissed_at": { + "name": "gsc_nudge_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "user_onboarding_answers_organization_idx": { + "name": "user_onboarding_answers_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_onboarding_answers_user_id_user_id_fk": { + "name": "user_onboarding_answers_user_id_user_id_fk", + "tableFrom": "user_onboarding_answers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_onboarding_answers_organization_id_organization_id_fk": { + "name": "user_onboarding_answers_organization_id_organization_id_fk", + "tableFrom": "user_onboarding_answers", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_issues": { + "name": "audit_issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "page_url": { + "name": "page_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_type": { + "name": "issue_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "details_json": { + "name": "details_json", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "audit_issues_audit_type_idx": { + "name": "audit_issues_audit_type_idx", + "columns": [ + { + "expression": "audit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_issues_page_id_idx": { + "name": "audit_issues_page_id_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_issues_audit_id_audits_id_fk": { + "name": "audit_issues_audit_id_audits_id_fk", + "tableFrom": "audit_issues", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_issues_page_id_audit_pages_id_fk": { + "name": "audit_issues_page_id_audit_pages_id_fk", + "tableFrom": "audit_issues", + "tableTo": "audit_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_lighthouse_results": { + "name": "audit_lighthouse_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "performance_score": { + "name": "performance_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "accessibility_score": { + "name": "accessibility_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "best_practices_score": { + "name": "best_practices_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seo_score": { + "name": "seo_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lcp_ms": { + "name": "lcp_ms", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cls": { + "name": "cls", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "inp_ms": { + "name": "inp_ms", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_size_bytes": { + "name": "payload_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "audit_lighthouse_results_audit_id_idx": { + "name": "audit_lighthouse_results_audit_id_idx", + "columns": [ + { + "expression": "audit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_lighthouse_results_page_id_idx": { + "name": "audit_lighthouse_results_page_id_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_lighthouse_results_audit_id_audits_id_fk": { + "name": "audit_lighthouse_results_audit_id_audits_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_lighthouse_results_page_id_audit_pages_id_fk": { + "name": "audit_lighthouse_results_page_id_audit_pages_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audit_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_links": { + "name": "audit_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_page_id": { + "name": "source_page_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_url": { + "name": "target_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_internal": { + "name": "is_internal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_nofollow": { + "name": "is_nofollow", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "audit_links_audit_target_idx": { + "name": "audit_links_audit_target_idx", + "columns": [ + { + "expression": "audit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_links_source_page_id_idx": { + "name": "audit_links_source_page_id_idx", + "columns": [ + { + "expression": "source_page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_links_audit_id_audits_id_fk": { + "name": "audit_links_audit_id_audits_id_fk", + "tableFrom": "audit_links", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_links_source_page_id_audit_pages_id_fk": { + "name": "audit_links_source_page_id_audit_pages_id_fk", + "tableFrom": "audit_links", + "tableTo": "audit_pages", + "columnsFrom": [ + "source_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_pages": { + "name": "audit_pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meta_description": { + "name": "meta_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "robots_meta": { + "name": "robots_meta", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "og_title": { + "name": "og_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "og_description": { + "name": "og_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "og_image": { + "name": "og_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "h1_count": { + "name": "h1_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h2_count": { + "name": "h2_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h3_count": { + "name": "h3_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h4_count": { + "name": "h4_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h5_count": { + "name": "h5_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "h6_count": { + "name": "h6_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "heading_order_json": { + "name": "heading_order_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "word_count": { + "name": "word_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "images_total": { + "name": "images_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "images_missing_alt": { + "name": "images_missing_alt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "images_json": { + "name": "images_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_link_count": { + "name": "internal_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_link_count": { + "name": "external_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "has_structured_data": { + "name": "has_structured_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hreflang_tags_json": { + "name": "hreflang_tags_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_indexable": { + "name": "is_indexable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "x_robots_tag": { + "name": "x_robots_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "header_canonical_url": { + "name": "header_canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crawl_depth": { + "name": "crawl_depth", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "in_sitemap": { + "name": "in_sitemap", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetch_class": { + "name": "fetch_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ok'" + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "audit_pages_audit_url_idx": { + "name": "audit_pages_audit_url_idx", + "columns": [ + { + "expression": "audit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_pages_audit_id_audits_id_fk": { + "name": "audit_pages_audit_id_audits_id_fk", + "tableFrom": "audit_pages", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audits": { + "name": "audits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_url": { + "name": "start_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "pages_crawled": { + "name": "pages_crawled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pages_total": { + "name": "pages_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lighthouse_total": { + "name": "lighthouse_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lighthouse_completed": { + "name": "lighthouse_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lighthouse_failed": { + "name": "lighthouse_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "current_phase": { + "name": "current_phase", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'discovery'" + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "audits_project_id_idx": { + "name": "audits_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audits_started_by_user_id_idx": { + "name": "audits_started_by_user_id_idx", + "columns": [ + { + "expression": "started_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audits_project_id_projects_id_fk": { + "name": "audits_project_id_projects_id_fk", + "tableFrom": "audits", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sam_project_memory": { + "name": "sam_project_memory", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": {}, + "foreignKeys": { + "sam_project_memory_project_id_projects_id_fk": { + "name": "sam_project_memory_project_id_projects_id_fk", + "tableFrom": "sam_project_memory", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sam_project_memory_project_id_label_pk": { + "name": "sam_project_memory_project_id_label_pk", + "columns": [ + "project_id", + "label" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sam_sessions": { + "name": "sam_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'New chat'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sam_sessions_project_updated_idx": { + "name": "sam_sessions_project_updated_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sam_sessions_project_id_projects_id_fk": { + "name": "sam_sessions_project_id_projects_id_fk", + "tableFrom": "sam_sessions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sam_sessions_user_id_user_id_fk": { + "name": "sam_sessions_user_id_user_id_fk", + "tableFrom": "sam_sessions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_accountId_providerId_idx": { + "name": "account_accountId_providerId_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "analytics_opted_out": { + "name": "analytics_opted_out", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expiresAt_idx": { + "name": "verification_expiresAt_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_customer_status": { + "name": "billing_customer_status", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "is_paying": { + "name": "is_paying", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "paid_plan_id": { + "name": "paid_plan_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paid_plan_status": { + "name": "paid_plan_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_json": { + "name": "customer_json", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_customer_status_organization_id_organization_id_fk": { + "name": "billing_customer_status_organization_id_organization_id_fk", + "tableFrom": "billing_customer_status", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gsc_connections": { + "name": "gsc_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gsc_account_id": { + "name": "gsc_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connected_account_email": { + "name": "connected_account_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "gsc_connections_project_idx": { + "name": "gsc_connections_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gsc_connections_organization_idx": { + "name": "gsc_connections_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gsc_connections_project_id_projects_id_fk": { + "name": "gsc_connections_project_id_projects_id_fk", + "tableFrom": "gsc_connections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gsc_connections_organization_id_organization_id_fk": { + "name": "gsc_connections_organization_id_organization_id_fk", + "tableFrom": "gsc_connections", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reddit_attributions": { + "name": "reddit_attributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_id": { + "name": "click_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_page": { + "name": "landing_page", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signup_sent_at": { + "name": "signup_sent_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purchase_sent_at": { + "name": "purchase_sent_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "to_char(now() AT TIME ZONE 'utc', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"')" + } + }, + "indexes": { + "reddit_attributions_user_idx": { + "name": "reddit_attributions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reddit_attributions_organization_idx": { + "name": "reddit_attributions_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reddit_attributions_user_id_user_id_fk": { + "name": "reddit_attributions_user_id_user_id_fk", + "tableFrom": "reddit_attributions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reddit_attributions_organization_id_organization_id_fk": { + "name": "reddit_attributions_organization_id_organization_id_fk", + "tableFrom": "reddit_attributions", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telemetry_state": { + "name": "telemetry_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "install_id": { + "name": "install_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_version": { + "name": "last_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tool_call_count": { + "name": "mcp_tool_call_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/_journal.json b/drizzle-pg/meta/_journal.json index 06c363d..b2a9907 100644 --- a/drizzle-pg/meta/_journal.json +++ b/drizzle-pg/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1784410131656, "tag": "0011_friendly_morlun", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1784423181488, + "tag": "0012_dashboard", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/0035_dashboard.sql b/drizzle/0035_dashboard.sql new file mode 100644 index 0000000..98d110f --- /dev/null +++ b/drizzle/0035_dashboard.sql @@ -0,0 +1,32 @@ +CREATE TABLE `backlink_snapshots` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `project_id` text NOT NULL, + `domain` text NOT NULL, + `rank` integer, + `backlinks` integer, + `referring_domains` integer, + `broken_backlinks` integer, + `new_backlinks` integer, + `lost_backlinks` integer, + `new_referring_domains` integer, + `lost_referring_domains` integer, + `captured_at` text DEFAULT (current_timestamp) NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `backlink_snapshots_project_captured_idx` ON `backlink_snapshots` (`project_id`,`captured_at`);--> statement-breakpoint +CREATE TABLE `organization_activation_state` ( + `organization_id` text PRIMARY KEY NOT NULL, + `first_mcp_authorized_at` text, + `first_mcp_tool_call_at` text, + `updated_at` text DEFAULT (current_timestamp) NOT NULL, + FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `project_activation_state` ( + `project_id` text PRIMARY KEY NOT NULL, + `competitor_step_clicked_at` text, + `mcp_card_dismissed_at` text, + `updated_at` text DEFAULT (current_timestamp) NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/drizzle/meta/0035_snapshot.json b/drizzle/meta/0035_snapshot.json new file mode 100644 index 0000000..22334a2 --- /dev/null +++ b/drizzle/meta/0035_snapshot.json @@ -0,0 +1,3373 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "85fa584d-1361-48f4-a0c6-0709507d9124", + "prevId": "28d529a7-31cc-448f-b468-8a4f86e45e12", + "tables": { + "backlink_snapshots": { + "name": "backlink_snapshots", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backlinks": { + "name": "backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referring_domains": { + "name": "referring_domains", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "broken_backlinks": { + "name": "broken_backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_backlinks": { + "name": "new_backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lost_backlinks": { + "name": "lost_backlinks", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_referring_domains": { + "name": "new_referring_domains", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lost_referring_domains": { + "name": "lost_referring_domains", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "captured_at": { + "name": "captured_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "backlink_snapshots_project_captured_idx": { + "name": "backlink_snapshots_project_captured_idx", + "columns": [ + "project_id", + "captured_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "backlink_snapshots_project_id_projects_id_fk": { + "name": "backlink_snapshots_project_id_projects_id_fk", + "tableFrom": "backlink_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "keyword_metrics": { + "name": "keyword_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "competition": { + "name": "competition", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_searches": { + "name": "monthly_searches", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "keyword_metrics_unique_project_keyword_location_language": { + "name": "keyword_metrics_unique_project_keyword_location_language", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code" + ], + "isUnique": true + }, + "keyword_metrics_lookup_idx": { + "name": "keyword_metrics_lookup_idx", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code", + "fetched_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "keyword_metrics_project_id_projects_id_fk": { + "name": "keyword_metrics_project_id_projects_id_fk", + "tableFrom": "keyword_metrics", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization_activation_state": { + "name": "organization_activation_state", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "first_mcp_authorized_at": { + "name": "first_mcp_authorized_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_mcp_tool_call_at": { + "name": "first_mcp_tool_call_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_activation_state_organization_id_organization_id_fk": { + "name": "organization_activation_state_organization_id_organization_id_fk", + "tableFrom": "organization_activation_state", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_activation_state": { + "name": "project_activation_state", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "competitor_step_clicked_at": { + "name": "competitor_step_clicked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcp_card_dismissed_at": { + "name": "mcp_card_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "project_activation_state_project_id_projects_id_fk": { + "name": "project_activation_state_project_id_projects_id_fk", + "tableFrom": "project_activation_state", + "tableTo": "projects", + "columnsFrom": [ + "project_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 + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_one_default_per_organization_idx": { + "name": "projects_one_default_per_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": true, + "where": "\"projects\".\"name\" = 'Default' AND \"projects\".\"domain\" IS NULL AND \"projects\".\"archived_at\" IS NULL" + }, + "projects_organization_id_idx": { + "name": "projects_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_check_runs": { + "name": "rank_check_runs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "keywords_total": { + "name": "keywords_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "keywords_checked": { + "name": "keywords_checked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_subset_run": { + "name": "is_subset_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rank_check_runs_config_idx": { + "name": "rank_check_runs_config_idx", + "columns": [ + "config_id", + "started_at" + ], + "isUnique": false + }, + "rank_check_runs_project_idx": { + "name": "rank_check_runs_project_idx", + "columns": [ + "project_id", + "started_at" + ], + "isUnique": false + }, + "rank_check_runs_one_active_per_config_idx": { + "name": "rank_check_runs_one_active_per_config_idx", + "columns": [ + "config_id" + ], + "isUnique": true, + "where": "\"rank_check_runs\".\"status\" IN ('pending', 'running')" + } + }, + "foreignKeys": { + "rank_check_runs_config_id_rank_tracking_configs_id_fk": { + "name": "rank_check_runs_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rank_check_runs_project_id_projects_id_fk": { + "name": "rank_check_runs_project_id_projects_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_snapshots": { + "name": "rank_snapshots", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tracking_keyword_id": { + "name": "tracking_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "serp_features": { + "name": "serp_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_snapshots_keyword_device_idx": { + "name": "rank_snapshots_keyword_device_idx", + "columns": [ + "tracking_keyword_id", + "device", + "checked_at" + ], + "isUnique": false + }, + "rank_snapshots_run_keyword_device_idx": { + "name": "rank_snapshots_run_keyword_device_idx", + "columns": [ + "run_id", + "tracking_keyword_id", + "device" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_snapshots_run_id_rank_check_runs_id_fk": { + "name": "rank_snapshots_run_id_rank_check_runs_id_fk", + "tableFrom": "rank_snapshots", + "tableTo": "rank_check_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_tracking_configs": { + "name": "rank_tracking_configs", + "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 + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "devices": { + "name": "devices", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'both'" + }, + "serp_depth": { + "name": "serp_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_interval": { + "name": "schedule_interval", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'weekly'" + }, + "location_name": { + "name": "location_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_skip_reason": { + "name": "last_skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_tracking_configs_project_active_created_idx": { + "name": "rank_tracking_configs_project_active_created_idx", + "columns": [ + "project_id", + "is_active", + "created_at" + ], + "isUnique": false + }, + "rank_tracking_configs_national_idx": { + "name": "rank_tracking_configs_national_idx", + "columns": [ + "project_id", + "domain", + "location_code" + ], + "isUnique": true, + "where": "\"rank_tracking_configs\".\"location_name\" IS NULL" + }, + "rank_tracking_configs_local_idx": { + "name": "rank_tracking_configs_local_idx", + "columns": [ + "project_id", + "domain", + "location_code", + "location_name" + ], + "isUnique": true, + "where": "\"rank_tracking_configs\".\"location_name\" IS NOT NULL" + } + }, + "foreignKeys": { + "rank_tracking_configs_project_id_projects_id_fk": { + "name": "rank_tracking_configs_project_id_projects_id_fk", + "tableFrom": "rank_tracking_configs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_tracking_keywords": { + "name": "rank_tracking_keywords", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metrics_fetched_at": { + "name": "metrics_fetched_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_tracking_keywords_config_keyword_idx": { + "name": "rank_tracking_keywords_config_keyword_idx", + "columns": [ + "config_id", + "keyword" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk": { + "name": "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_tracking_keywords", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keyword_tag_assignments": { + "name": "saved_keyword_tag_assignments", + "columns": { + "saved_keyword_id": { + "name": "saved_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keyword_tag_assignments_unique_idx": { + "name": "saved_keyword_tag_assignments_unique_idx", + "columns": [ + "saved_keyword_id", + "tag_id" + ], + "isUnique": true + }, + "saved_keyword_tag_assignments_tag_idx": { + "name": "saved_keyword_tag_assignments_tag_idx", + "columns": [ + "tag_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk": { + "name": "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keywords", + "columnsFrom": [ + "saved_keyword_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk": { + "name": "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keyword_tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keyword_tags": { + "name": "saved_keyword_tags", + "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 + }, + "normalized_name": { + "name": "normalized_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "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, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keyword_tags_project_normalized_name_idx": { + "name": "saved_keyword_tags_project_normalized_name_idx", + "columns": [ + "project_id", + "normalized_name" + ], + "isUnique": true + }, + "saved_keyword_tags_project_name_idx": { + "name": "saved_keyword_tags_project_name_idx", + "columns": [ + "project_id", + "name" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keyword_tags_project_id_projects_id_fk": { + "name": "saved_keyword_tags_project_id_projects_id_fk", + "tableFrom": "saved_keyword_tags", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keywords": { + "name": "saved_keywords", + "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 + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keywords_unique_project_keyword_location_language": { + "name": "saved_keywords_unique_project_keyword_location_language", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code" + ], + "isUnique": true + }, + "saved_keywords_project_created_idx": { + "name": "saved_keywords_project_created_idx", + "columns": [ + "project_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keywords_project_id_projects_id_fk": { + "name": "saved_keywords_project_id_projects_id_fk", + "tableFrom": "saved_keywords", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_onboarding_answers": { + "name": "user_onboarding_answers", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interested_features": { + "name": "interested_features", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "work_for": { + "name": "work_for", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_website_count": { + "name": "client_website_count", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "found_via": { + "name": "found_via", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcp_setup_intent": { + "name": "mcp_setup_intent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gsc_nudge_dismissed_at": { + "name": "gsc_nudge_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "user_onboarding_answers_organization_idx": { + "name": "user_onboarding_answers_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_onboarding_answers_user_id_user_id_fk": { + "name": "user_onboarding_answers_user_id_user_id_fk", + "tableFrom": "user_onboarding_answers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_onboarding_answers_organization_id_organization_id_fk": { + "name": "user_onboarding_answers_organization_id_organization_id_fk", + "tableFrom": "user_onboarding_answers", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_issues": { + "name": "audit_issues", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_url": { + "name": "page_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "issue_type": { + "name": "issue_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'info'" + }, + "details_json": { + "name": "details_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_issues_audit_type_idx": { + "name": "audit_issues_audit_type_idx", + "columns": [ + "audit_id", + "issue_type" + ], + "isUnique": false + }, + "audit_issues_page_id_idx": { + "name": "audit_issues_page_id_idx", + "columns": [ + "page_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_issues_audit_id_audits_id_fk": { + "name": "audit_issues_audit_id_audits_id_fk", + "tableFrom": "audit_issues", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_issues_page_id_audit_pages_id_fk": { + "name": "audit_issues_page_id_audit_pages_id_fk", + "tableFrom": "audit_issues", + "tableTo": "audit_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_lighthouse_results": { + "name": "audit_lighthouse_results", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "performance_score": { + "name": "performance_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessibility_score": { + "name": "accessibility_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "best_practices_score": { + "name": "best_practices_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seo_score": { + "name": "seo_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lcp_ms": { + "name": "lcp_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cls": { + "name": "cls", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inp_ms": { + "name": "inp_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_size_bytes": { + "name": "payload_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_lighthouse_results_audit_id_idx": { + "name": "audit_lighthouse_results_audit_id_idx", + "columns": [ + "audit_id" + ], + "isUnique": false + }, + "audit_lighthouse_results_page_id_idx": { + "name": "audit_lighthouse_results_page_id_idx", + "columns": [ + "page_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_lighthouse_results_audit_id_audits_id_fk": { + "name": "audit_lighthouse_results_audit_id_audits_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_lighthouse_results_page_id_audit_pages_id_fk": { + "name": "audit_lighthouse_results_page_id_audit_pages_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audit_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_links": { + "name": "audit_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_page_id": { + "name": "source_page_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_url": { + "name": "target_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_internal": { + "name": "is_internal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "is_nofollow": { + "name": "is_nofollow", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "audit_links_audit_target_idx": { + "name": "audit_links_audit_target_idx", + "columns": [ + "audit_id", + "target_url" + ], + "isUnique": false + }, + "audit_links_source_page_id_idx": { + "name": "audit_links_source_page_id_idx", + "columns": [ + "source_page_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_links_audit_id_audits_id_fk": { + "name": "audit_links_audit_id_audits_id_fk", + "tableFrom": "audit_links", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_links_source_page_id_audit_pages_id_fk": { + "name": "audit_links_source_page_id_audit_pages_id_fk", + "tableFrom": "audit_links", + "tableTo": "audit_pages", + "columnsFrom": [ + "source_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_pages": { + "name": "audit_pages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_description": { + "name": "meta_description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "robots_meta": { + "name": "robots_meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_title": { + "name": "og_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_description": { + "name": "og_description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_image": { + "name": "og_image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "h1_count": { + "name": "h1_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h2_count": { + "name": "h2_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h3_count": { + "name": "h3_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h4_count": { + "name": "h4_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h5_count": { + "name": "h5_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h6_count": { + "name": "h6_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "heading_order_json": { + "name": "heading_order_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "word_count": { + "name": "word_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_total": { + "name": "images_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_missing_alt": { + "name": "images_missing_alt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_json": { + "name": "images_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_link_count": { + "name": "internal_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "external_link_count": { + "name": "external_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "has_structured_data": { + "name": "has_structured_data", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "hreflang_tags_json": { + "name": "hreflang_tags_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_indexable": { + "name": "is_indexable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "x_robots_tag": { + "name": "x_robots_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "header_canonical_url": { + "name": "header_canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawl_depth": { + "name": "crawl_depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "in_sitemap": { + "name": "in_sitemap", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fetch_class": { + "name": "fetch_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ok'" + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_pages_audit_url_idx": { + "name": "audit_pages_audit_url_idx", + "columns": [ + "audit_id", + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_pages_audit_id_audits_id_fk": { + "name": "audit_pages_audit_id_audits_id_fk", + "tableFrom": "audit_pages", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audits": { + "name": "audits", + "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 + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_url": { + "name": "start_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'running'" + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "pages_crawled": { + "name": "pages_crawled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "pages_total": { + "name": "pages_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_total": { + "name": "lighthouse_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_completed": { + "name": "lighthouse_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_failed": { + "name": "lighthouse_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_phase": { + "name": "current_phase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'discovery'" + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audits_project_id_idx": { + "name": "audits_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "audits_started_by_user_id_idx": { + "name": "audits_started_by_user_id_idx", + "columns": [ + "started_by_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audits_project_id_projects_id_fk": { + "name": "audits_project_id_projects_id_fk", + "tableFrom": "audits", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sam_project_memory": { + "name": "sam_project_memory", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "sam_project_memory_project_id_projects_id_fk": { + "name": "sam_project_memory_project_id_projects_id_fk", + "tableFrom": "sam_project_memory", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sam_project_memory_project_id_label_pk": { + "columns": [ + "project_id", + "label" + ], + "name": "sam_project_memory_project_id_label_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sam_sessions": { + "name": "sam_sessions", + "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 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'New chat'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sam_sessions_project_updated_idx": { + "name": "sam_sessions_project_updated_idx", + "columns": [ + "project_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sam_sessions_project_id_projects_id_fk": { + "name": "sam_sessions_project_id_projects_id_fk", + "tableFrom": "sam_sessions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sam_sessions_user_id_user_id_fk": { + "name": "sam_sessions_user_id_user_id_fk", + "tableFrom": "sam_sessions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "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 + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "account_accountId_providerId_idx": { + "name": "account_accountId_providerId_idx", + "columns": [ + "account_id", + "provider_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "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 + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "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, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "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 + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "analytics_opted_out": { + "name": "analytics_opted_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + }, + "verification_expiresAt_idx": { + "name": "verification_expiresAt_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "billing_customer_status": { + "name": "billing_customer_status", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_paying": { + "name": "is_paying", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "paid_plan_id": { + "name": "paid_plan_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paid_plan_status": { + "name": "paid_plan_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_json": { + "name": "customer_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synced_at": { + "name": "synced_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_customer_status_organization_id_organization_id_fk": { + "name": "billing_customer_status_organization_id_organization_id_fk", + "tableFrom": "billing_customer_status", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "gsc_connections": { + "name": "gsc_connections", + "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 + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gsc_account_id": { + "name": "gsc_account_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connected_account_email": { + "name": "connected_account_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "gsc_connections_project_idx": { + "name": "gsc_connections_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true + }, + "gsc_connections_organization_idx": { + "name": "gsc_connections_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "gsc_connections_project_id_projects_id_fk": { + "name": "gsc_connections_project_id_projects_id_fk", + "tableFrom": "gsc_connections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gsc_connections_organization_id_organization_id_fk": { + "name": "gsc_connections_organization_id_organization_id_fk", + "tableFrom": "gsc_connections", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reddit_attributions": { + "name": "reddit_attributions", + "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 + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "click_id": { + "name": "click_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "landing_page": { + "name": "landing_page", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signup_sent_at": { + "name": "signup_sent_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purchase_sent_at": { + "name": "purchase_sent_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "reddit_attributions_user_idx": { + "name": "reddit_attributions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "reddit_attributions_organization_idx": { + "name": "reddit_attributions_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reddit_attributions_user_id_user_id_fk": { + "name": "reddit_attributions_user_id_user_id_fk", + "tableFrom": "reddit_attributions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reddit_attributions_organization_id_organization_id_fk": { + "name": "reddit_attributions_organization_id_organization_id_fk", + "tableFrom": "reddit_attributions", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "telemetry_state": { + "name": "telemetry_state", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "install_id": { + "name": "install_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_version": { + "name": "last_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcp_tool_call_count": { + "name": "mcp_tool_call_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "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 3e419f8..085f980 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -246,6 +246,13 @@ "when": 1784410130115, "tag": "0034_wonderful_skrulls", "breakpoints": true + }, + { + "idx": 35, + "version": "6", + "when": 1784423179842, + "tag": "0035_dashboard", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/client/components/Sidebar.tsx b/src/client/components/Sidebar.tsx index 6542316..3cf0992 100644 --- a/src/client/components/Sidebar.tsx +++ b/src/client/components/Sidebar.tsx @@ -57,9 +57,9 @@ function SidebarNavLink({ }) { return ( @@ -109,6 +109,15 @@ export function Sidebar({ projectId, onNavigate, onClose }: SidebarProps) { } }; + // Coming back from Chat, land on the dashboard rather than leaving the + // conversation filling the content panel next to a Browse nav. + const openBrowse = () => { + setView("browse"); + if (!projectId || !onSamRoute) return; + void navigate({ to: "/p/$projectId", params: { projectId } }); + onNavigate?.(); + }; + return (
@@ -147,7 +156,7 @@ export function Sidebar({ projectId, onNavigate, onClose }: SidebarProps) { icon={LayoutGrid} label="Browse" active={view === "browse"} - onClick={() => setView("browse")} + onClick={openBrowse} /> = Object.fromEntries( + Object.entries(AUDIT_ISSUE_TYPES).map(([key, value]) => [key, value.title]), +); + +export function GscCard({ + projectId, + connected, +}: { + projectId: string; + connected: boolean; +}) { + const reportQuery = useQuery({ + queryKey: ["dashboardGscReport", projectId], + queryFn: () => + getSearchPerformanceReport({ + data: { projectId, dateRange: "last_28_days" }, + }), + enabled: connected, + }); + + // Not connected (or a dead grant discovered by the report call): the + // connection card sells and runs the whole flow itself. + if (!connected || (reportQuery.data && !reportQuery.data.connected)) { + return ( +
+ +
+ ); + } + + const report = reportQuery.data; + + return ( + + More details + + } + > + {reportQuery.isPending ? ( +
+ {Array.from({ length: 4 }, (_, i) => ( +
+ ))} +
+ ) : reportQuery.isError ? ( +

+ Couldn’t load Search Console data. Try again shortly. +

+ ) : report?.connected ? ( +
+ + } + /> + + } + /> + + +
+ ) : null} + + ); +} + +export function RankMovementCard({ + projectId, + rank, +}: { + projectId: string; + rank: DashboardRankSummary | null; +}) { + if (!rank) { + return ( + + + Track your first keyword + + } + /> + + ); + } + + return ( + + More details + + } + > +
+ + 0 ? "success" : undefined} + /> + 0 ? "error" : undefined} + /> + +
+
+ ); +} + +export function AuditHealthCard({ + projectId, + audit, +}: { + projectId: string; + audit: DashboardAuditSummary | null; +}) { + if (!audit) { + return ( + + + Run an audit + + } + /> + + ); + } + + return ( + + More details + + } + > + {audit.topIssues.length === 0 ? ( +
+ + No issues found — your site looks healthy. +
+ ) : ( +
    + {audit.topIssues.map((issue) => ( +
  • + + + + {issueTitles[issue.issueType] ?? issue.issueType} + + + + {issue.count} {issue.count === 1 ? "page" : "pages"} + +
  • + ))} + {audit.totalIssueTypes > audit.topIssues.length ? ( +
  • + + {audit.totalIssueTypes - audit.topIssues.length} more issue + {audit.totalIssueTypes - audit.topIssues.length === 1 ? "" : "s"} +
  • + ) : null} +
+ )} +
+ ); +} + +export function BacklinkPulseCard({ + projectId, + backlinks, + refreshing, +}: { + projectId: string; + backlinks: DashboardBacklinkSummary | null; + refreshing: boolean; +}) { + if (!backlinks && refreshing) { + return ( + +
+ {Array.from({ length: 4 }, (_, i) => ( +
+ ))} +
+ + ); + } + + if (!backlinks) { + return ( + +

+ We’ll snapshot who links to your domain — nothing to set up. +

+
+ ); + } + + return ( + + More details + + } + > +
+ + + 0 + ? "success" + : undefined + } + /> + 0 + ? "error" + : undefined + } + /> +
+
+ ); +} diff --git a/src/client/features/dashboard/DashboardPage.tsx b/src/client/features/dashboard/DashboardPage.tsx new file mode 100644 index 0000000..4058ba4 --- /dev/null +++ b/src/client/features/dashboard/DashboardPage.tsx @@ -0,0 +1,376 @@ +import { useEffect, useRef, useState } from "react"; +import { Link, useNavigate } from "@tanstack/react-router"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { ChevronLeft, ChevronRight, Check } from "lucide-react"; +import { captureClientEvent } from "@/client/lib/posthog"; +import { + computeNextStep, + isStepDone, + STEP_ORDER, +} from "@/client/features/dashboard/dashboardSteps"; +import { + AuditHealthCard, + BacklinkPulseCard, + GscCard, + RankMovementCard, +} from "@/client/features/dashboard/DashboardCards"; +import { McpConnectCard } from "@/client/features/dashboard/McpConnectCard"; +import { getStandardErrorMessage } from "@/client/lib/error-messages"; +import type { DashboardActivation } from "@/server/features/dashboard/services/DashboardService"; +import { + getDashboardActivation, + getDashboardOverview, + markDashboardCompetitorClicked, + refreshDashboardBacklinkSnapshot, +} from "@/serverFunctions/dashboard"; +import { setProjectDomain } from "@/serverFunctions/projects"; +import type { DashboardHeroStep } from "@/types/schemas/dashboard"; + +const HERO_COPY: Record< + DashboardHeroStep, + { title: string; body: string; cta: string } +> = { + domain: { + title: "What site are you working on?", + body: "Set your project's domain and every card on this page starts working for it — backlinks, audits, rank tracking.", + cta: "Save", + }, + mcp: { + title: "Connect your AI agent", + body: "OpenSEO is built to be used from agents like Claude. Connect once, then ask it to use OpenSEO to help build your SEO strategy.", + cta: "Show me how", + }, + gsc: { + title: "Connect Search Console", + body: "Your real queries and clicks, straight from Google.", + cta: "Connect", + }, + competitor: { + title: "Size up a competitor", + body: "Paste a competitor's domain to see what they rank for and who links to them.", + cta: "Open domain lookup", + }, +}; + +function scrollToCard(id: string) { + document.getElementById(id)?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); +} + +// Users paste full URLs; store the bare host like settings expects. +function normalizeDomainInput(value: string): string { + return value + .trim() + .replace(/^https?:\/\//i, "") + .replace(/\/.*$/, ""); +} + +function OnboardingChecklist({ + projectId, + activation, +}: { + projectId: string; + activation: DashboardActivation; +}) { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const [domainInput, setDomainInput] = useState(""); + // null = follow the first actionable step; set once the user pages with ‹ ›. + const [viewedIndex, setViewedIndex] = useState(null); + const invalidateActivation = () => + void queryClient.invalidateQueries({ + queryKey: ["dashboardActivation", projectId], + }); + + const competitorClickMutation = useMutation({ + mutationFn: () => markDashboardCompetitorClicked({ data: { projectId } }), + onSuccess: invalidateActivation, + }); + const domainMutation = useMutation({ + mutationFn: (domain: string) => + setProjectDomain({ data: { projectId, domain } }), + onSuccess: () => { + invalidateActivation(); + void queryClient.invalidateQueries({ + queryKey: ["dashboardOverview", projectId], + }); + void queryClient.invalidateQueries({ queryKey: ["projects"] }); + }, + onError: (error) => + toast.error( + getStandardErrorMessage(error, "Couldn't save the domain. Try again."), + ), + }); + + // Hidden once every step is done. + const nextStep = computeNextStep(activation); + if (!nextStep) return null; + + const index = viewedIndex ?? STEP_ORDER.indexOf(nextStep); + const step = STEP_ORDER[index]; + const copy = HERO_COPY[step]; + const done = isStepDone(activation, step); + + const page = (delta: number) => + setViewedIndex(Math.min(Math.max(index + delta, 0), STEP_ORDER.length - 1)); + + const onSubmitDomain = () => { + const domain = normalizeDomainInput(domainInput); + if (!domain) return; + captureClientEvent("dashboard:next_move_click", { step: "domain" }); + domainMutation.mutate(domain); + }; + + // Only the gsc/competitor steps use the fallback CTA button — domain + // renders an inline form and mcp renders a Link. + const onCta = () => { + captureClientEvent("dashboard:next_move_click", { step }); + if (step === "gsc") { + scrollToCard("connect-gsc"); + } else if (step === "competitor") { + competitorClickMutation.mutate(); + void navigate({ to: "/p/$projectId/domain", params: { projectId } }); + } + }; + + return ( +
+
+

+ Onboarding checklist +

+
+ + + {index + 1} / {STEP_ORDER.length} + + +
+
+
+
+

{copy.title}

+

+ {copy.body} +

+
+
+ {done ? ( + + + Done + + ) : step === "domain" ? ( +
{ + event.preventDefault(); + onSubmitDomain(); + }} + > + setDomainInput(event.target.value)} + aria-label="Your site's domain" + /> + +
+ ) : step === "mcp" ? ( + + captureClientEvent("dashboard:next_move_click", { step }) + } + > + {copy.cta} → + + ) : ( + + )} +
+
+
+ ); +} + +export function DashboardPage({ projectId }: { projectId: string }) { + const queryClient = useQueryClient(); + + const activationQuery = useQuery({ + queryKey: ["dashboardActivation", projectId], + queryFn: () => getDashboardActivation({ data: { projectId } }), + }); + const overviewQuery = useQuery({ + queryKey: ["dashboardOverview", projectId], + queryFn: () => getDashboardOverview({ data: { projectId } }), + }); + + const activation = activationQuery.data; + const overview = overviewQuery.data; + + // Visit-triggered backlink snapshot: fire once per page view when the + // overview reports a missing or stale snapshot for a project with a domain. + // The server re-checks freshness, so a stray double-fire costs nothing. + const refreshMutation = useMutation({ + mutationFn: () => refreshDashboardBacklinkSnapshot({ data: { projectId } }), + onSuccess: () => + void queryClient.invalidateQueries({ + queryKey: ["dashboardOverview", projectId], + }), + }); + const refreshFiredRef = useRef(false); + const needsSnapshot = + activation?.domain != null && + overview !== undefined && + (overview.backlinks === null || overview.backlinks.stale); + useEffect(() => { + if (!needsSnapshot || refreshFiredRef.current) return; + refreshFiredRef.current = true; + refreshMutation.mutate(); + }, [needsSnapshot, refreshMutation]); + + if (activationQuery.isError) { + return ( +
+
+ {getStandardErrorMessage(activationQuery.error)} +
+
+ ); + } + + if (!activation) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + const showBacklinks = activation.domain !== null; + const gscConnected = activation.gsc.connected; + + return ( +
+
+

Dashboard

+ + + + {/* Every card is half width on large screens (only the checklist spans). + Cards with data render before setup pitches and empty states. */} +
+ {[ + // Array order is the within-bucket order after the data-first sort: + // the MCP pitch leads the setup cards. + ...(activation.mcp.firstToolCallAt || activation.mcp.cardDismissedAt + ? [] + : [ + { + key: "mcp", + hasData: false, + node: ( + + ), + }, + ]), + { + key: "gsc", + hasData: gscConnected, + node: , + }, + { + key: "rank", + hasData: overview?.rank != null, + node: ( + + ), + }, + { + key: "audit", + hasData: overview?.audit != null, + node: ( + + ), + }, + ...(showBacklinks + ? [ + { + key: "backlinks", + hasData: + overview?.backlinks != null || refreshMutation.isPending, + node: ( + + ), + }, + ] + : []), + ] + .toSorted((a, b) => Number(b.hasData) - Number(a.hasData)) + .map((card) => ( +
{card.node}
+ ))} +
+
+
+ ); +} diff --git a/src/client/features/dashboard/McpConnectCard.tsx b/src/client/features/dashboard/McpConnectCard.tsx new file mode 100644 index 0000000..860b0da --- /dev/null +++ b/src/client/features/dashboard/McpConnectCard.tsx @@ -0,0 +1,126 @@ +import { Link } from "@tanstack/react-router"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { CopyButton } from "@/client/features/ai-mcp/SetupControls"; +import { captureClientEvent } from "@/client/lib/posthog"; +import type { DashboardActivation } from "@/server/features/dashboard/services/DashboardService"; +import { dismissDashboardMcpCard } from "@/serverFunctions/dashboard"; + +function firstPrompts(domain: string | null): string[] { + const site = domain ?? "my site"; + return [ + `Review ${site}. Ideas for what keywords we could target? Use OpenSEO`, + "Research my competitors top pages and keywords and tell me what's working. Use OpenSEO", + ]; +} + +/** + * The MCP activation card. Pitches the agent workflow and links to the + * AI & MCP page for setup; disappears for good after the org's first + * external tool call (or an explicit "I already connected"). + */ +export function McpConnectCard({ + projectId, + activation, +}: { + projectId: string; + activation: DashboardActivation; +}) { + const queryClient = useQueryClient(); + const dismissMutation = useMutation({ + mutationFn: () => dismissDashboardMcpCard({ data: { projectId } }), + onSuccess: () => + void queryClient.invalidateQueries({ + queryKey: ["dashboardActivation", projectId], + }), + }); + + if (activation.mcp.firstToolCallAt || activation.mcp.cardDismissedAt) { + return null; + } + + const connected = activation.mcp.authorizedAt !== null; + + return ( +
+
+

+ Connect your AI agent +

+
+ {connected ? ( + + Connected + + ) : null} + +
+
+
+ {connected ? ( + <> +

+ Your agent is connected. Try asking it: +

+
    + {firstPrompts(activation.domain).map((prompt) => ( +
  • + + {prompt} + + + captureClientEvent("dashboard:mcp_prompt_copy") + } + /> +
  • + ))} +
+

+ Waiting for your first call — this card disappears once your agent + talks to OpenSEO. +

+ + ) : ( + <> +
+

+ OpenSEO is designed to give your AI agent the data it needs to + build a great SEO strategy and help you execute it. +

+

+ This way you aren’t limited on “AI credits”. +

+

+ You can work with your agent to figure out what automations make + sense for you and it can help you write content too. +

+
+ captureClientEvent("dashboard:mcp_setup_open")} + > + Set up in AI & MCP → + + + )} +
+
+ ); +} diff --git a/src/client/features/dashboard/cardParts.tsx b/src/client/features/dashboard/cardParts.tsx new file mode 100644 index 0000000..fe09e9f --- /dev/null +++ b/src/client/features/dashboard/cardParts.tsx @@ -0,0 +1,111 @@ +// Shared building blocks for the dashboard cards. Same visual language as +// the GSC IntegrationCard (rounded-xl, shadow-sm, header row + divider) so +// the embedded SearchConsoleConnectionCard doesn't read as a different +// design system. +export function CardShell({ + title, + stamp, + action, + children, +}: { + title: string; + stamp?: string; + action?: React.ReactNode; + children: React.ReactNode; +}) { + return ( +
+
+

{title}

+ {action} +
+
+ {children} + {stamp ? ( +

{stamp}

+ ) : null} +
+
+ ); +} + +export function EmptyCardBody({ + message, + cta, +}: { + message: string; + cta: React.ReactNode; +}) { + return ( +
+

{message}

+ {cta} +
+ ); +} + +export function Stat({ + label, + value, + tone, + sub, +}: { + label: string; + value: string; + tone?: "success" | "error"; + sub?: React.ReactNode; +}) { + const toneClass = + tone === "success" ? "text-success" : tone === "error" ? "text-error" : ""; + return ( +
+

+ {label} +

+

+ {value} +

+ {sub} +
+ ); +} + +export function PercentDelta({ + current, + previous, +}: { + current: number; + previous: number; +}) { + if (previous <= 0) return null; + const pct = ((current - previous) / previous) * 100; + if (!Number.isFinite(pct)) return null; + const rounded = Math.round(pct); + const tone = rounded > 0 ? "text-success" : rounded < 0 ? "text-error" : ""; + return ( +

+ {rounded > 0 ? "▲" : rounded < 0 ? "▼" : ""} {Math.abs(rounded)}% +

+ ); +} + +export const moreDetailsClass = "btn btn-ghost btn-xs"; + +export function newLost(value: number | null): string { + return value === null ? "—" : String(value); +} + +export function formatDay(timestamp: string): string { + const ms = Date.parse( + // SQLite's current_timestamp default has no timezone marker; treat it as + // UTC rather than letting the browser parse it as local time. + /^\d{4}-\d{2}-\d{2} /.test(timestamp) + ? `${timestamp.replace(" ", "T")}Z` + : timestamp, + ); + if (Number.isNaN(ms)) return timestamp; + return new Date(ms).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + }); +} diff --git a/src/client/features/dashboard/dashboardSteps.ts b/src/client/features/dashboard/dashboardSteps.ts new file mode 100644 index 0000000..aae78f2 --- /dev/null +++ b/src/client/features/dashboard/dashboardSteps.ts @@ -0,0 +1,42 @@ +import type { DashboardActivation } from "@/server/features/dashboard/services/DashboardService"; +import type { DashboardHeroStep } from "@/types/schemas/dashboard"; + +export const STEP_ORDER: DashboardHeroStep[] = [ + "domain", + "mcp", + "gsc", + "competitor", +]; + +// A step is "done" when the underlying product state exists, regardless of +// how it got there. The MCP hero step completes at authorization (the card +// below owns the connected-but-no-call-yet coaching) or when the user said +// "I already connected". +export function isStepDone( + activation: DashboardActivation, + step: DashboardHeroStep, +): boolean { + switch (step) { + case "domain": + return activation.domain !== null; + case "mcp": + return ( + activation.mcp.authorizedAt !== null || + activation.mcp.cardDismissedAt !== null + ); + case "gsc": + return activation.gsc.connected; + case "competitor": + return activation.competitorClickedAt !== null; + } +} + +/** The single step the hero should coach next; null = coaching is over. */ +export function computeNextStep( + activation: DashboardActivation, +): DashboardHeroStep | null { + for (const step of STEP_ORDER) { + if (!isStepDone(activation, step)) return step; + } + return null; +} diff --git a/src/client/features/gsc/SearchConsoleConnectionCard.tsx b/src/client/features/gsc/SearchConsoleConnectionCard.tsx index 251f434..ae70698 100644 --- a/src/client/features/gsc/SearchConsoleConnectionCard.tsx +++ b/src/client/features/gsc/SearchConsoleConnectionCard.tsx @@ -96,6 +96,14 @@ export function SearchConsoleConnectionCard({ void queryClient.invalidateQueries({ queryKey: ["searchPerformanceTable", projectId], }); + // The dashboard embeds this card and swaps it for the Search + // performance stats card once activation reports the connection. + void queryClient.invalidateQueries({ + queryKey: ["dashboardActivation", projectId], + }); + void queryClient.invalidateQueries({ + queryKey: ["dashboardGscReport", projectId], + }); }, onError: (error) => toast.error(getStandardErrorMessage(error)), }); @@ -116,6 +124,12 @@ export function SearchConsoleConnectionCard({ void queryClient.invalidateQueries({ queryKey: ["searchPerformanceTable", projectId], }); + void queryClient.invalidateQueries({ + queryKey: ["dashboardActivation", projectId], + }); + void queryClient.invalidateQueries({ + queryKey: ["dashboardGscReport", projectId], + }); }, onError: (error) => toast.error(getStandardErrorMessage(error)), }); @@ -177,7 +191,8 @@ export function SearchConsoleConnectionCard({ ) : (

- Connect your Google Search Console to get insights in OpenSEO. + Connect GSC to see how your website is actually performing in Google + Search.

+
+ + {step < ONBOARDING_LAST_STEP ? (
-
- ) : null} + ) : ( + + )} +
); } -function McpRecommendation({ - isSaving, - onBack, - onSetup, - onSkip, -}: { - isSaving: boolean; - onBack: () => void; - onSetup: () => void; - onSkip: () => void; -}) { - const capabilities = [ - "Keyword research", - "Competitor research", - "Link prospecting", - ]; - - return ( -
- -

Set up OpenSEO MCP?

-

- The most powerful way to use OpenSEO — use AI to supercharge your SEO - skills. -

- -
    - {capabilities.map((capability) => ( -
  • - - - - {capability} -
  • - ))} -
- - - -
- ); -} - function OnboardingChoiceGroup({ title, description, diff --git a/src/client/features/onboarding/onboardingModel.ts b/src/client/features/onboarding/onboardingModel.ts index 32bbb10..81080fd 100644 --- a/src/client/features/onboarding/onboardingModel.ts +++ b/src/client/features/onboarding/onboardingModel.ts @@ -1,7 +1,7 @@ import { queryOptions } from "@tanstack/react-query"; import { getOnboardingAnswers } from "@/serverFunctions/onboarding"; -export const ONBOARDING_LAST_STEP = 4; +export const ONBOARDING_LAST_STEP = 3; export const INTEREST_OPTIONS = [ "AI workflows with Claude or Codex (MCP)", @@ -32,6 +32,7 @@ export const CLIENT_WEBSITE_COUNT_OPTIONS = [ ] as const; export const SOURCE_OPTIONS = [ + "Product Hunt", "Google", "Reddit", "X / Twitter", @@ -110,7 +111,7 @@ export function restoreOnboardingAnswers( export function buildOnboardingPayload( answers: OnboardingAnswers, step: number, - extra: { mcpSetupIntent?: "yes" | "no"; completed?: boolean } = {}, + extra: { completed?: boolean } = {}, ) { const interestedFeatures = answers.selectedInterests.map((value) => value === "Other" && answers.interestOther.trim() diff --git a/src/client/features/projects/ProjectSwitcher.tsx b/src/client/features/projects/ProjectSwitcher.tsx index 1da25d2..6350632 100644 --- a/src/client/features/projects/ProjectSwitcher.tsx +++ b/src/client/features/projects/ProjectSwitcher.tsx @@ -30,7 +30,7 @@ export function ProjectSwitcher({ if (project.id === activeProjectId) return; setLastProjectId(project.id); void navigate({ - to: "/p/$projectId/keywords", + to: "/p/$projectId", params: { projectId: project.id }, }); }; diff --git a/src/client/navigation/items.ts b/src/client/navigation/items.ts index 8964635..b27d53e 100644 --- a/src/client/navigation/items.ts +++ b/src/client/navigation/items.ts @@ -3,6 +3,7 @@ import { Bot, ClipboardCheck, Globe, + LayoutDashboard, Link2, MessageSquare, Search, @@ -13,6 +14,14 @@ import { linkOptions } from "@tanstack/react-router"; import { GoogleGlyphMuted } from "@/client/features/gsc/GoogleGlyph"; const projectNavItems = [ + { + to: "/p/$projectId" as const, + label: "Dashboard", + icon: LayoutDashboard, + // Without exact matching, the index path is a prefix of every project + // route and the Dashboard item would render active everywhere. + activeOptions: { exact: true, includeSearch: false }, + }, { to: "/p/$projectId/keywords" as const, label: "Keyword Research", @@ -90,6 +99,10 @@ export function getProjectNavGroups(projectId: string) { all.find((i) => i.to === path)!; return [ + { + label: "Overview", + items: [byPath("/p/$projectId")], + }, { label: "Research", items: [ diff --git a/src/db/app.schema.ts b/src/db/app.schema.ts index 542868f..6709dad 100644 --- a/src/db/app.schema.ts +++ b/src/db/app.schema.ts @@ -350,3 +350,71 @@ export const rankSnapshots = sqliteTable( ), ], ); + +// Dashboard activation milestones. Organization-scoped: MCP OAuth grants are +// user-level, so any member connecting an external MCP client satisfies the +// milestone for the whole organization. Timestamps are first-occurrence only +// and never move once set. +export const organizationActivationState = sqliteTable( + "organization_activation_state", + { + organizationId: text("organization_id") + .primaryKey() + .references(() => organization.id, { onDelete: "cascade" }), + firstMcpAuthorizedAt: text("first_mcp_authorized_at"), + firstMcpToolCallAt: text("first_mcp_tool_call_at"), + updatedAt: text("updated_at") + .notNull() + .default(sql`(current_timestamp)`), + }, +); + +// Per-project state for the dashboard's onboarding checklist. Most steps +// complete via real product state (projects.domain, gsc_connections, MCP +// activation); the competitor step completes on click-through. +export const projectActivationState = sqliteTable("project_activation_state", { + projectId: text("project_id") + .primaryKey() + .references(() => projects.id, { onDelete: "cascade" }), + competitorStepClickedAt: text("competitor_step_clicked_at"), + // "I already connected" on the MCP card: hides the card for this project + // without faking the org-level first-tool-call milestone, which stays + // truthful and self-heals when a real external call lands. + mcpCardDismissedAt: text("mcp_card_dismissed_at"), + updatedAt: text("updated_at") + .notNull() + .default(sql`(current_timestamp)`), +}); + +// Point-in-time backlink profile summaries for the project's own domain, +// written by the dashboard's visit-triggered refresh. DataForSEO's summary +// already carries new/lost counts, so one snapshot renders a full card; +// rows accumulate into history for future trend views. The domain is stored +// per row so a later project-domain change doesn't rewrite history. +export const backlinkSnapshots = sqliteTable( + "backlink_snapshots", + { + id: integer("id").primaryKey({ autoIncrement: true }), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + domain: text("domain").notNull(), + rank: integer("rank"), + backlinks: integer("backlinks"), + referringDomains: integer("referring_domains"), + brokenBacklinks: integer("broken_backlinks"), + newBacklinks: integer("new_backlinks"), + lostBacklinks: integer("lost_backlinks"), + newReferringDomains: integer("new_referring_domains"), + lostReferringDomains: integer("lost_referring_domains"), + capturedAt: text("captured_at") + .notNull() + .default(sql`(current_timestamp)`), + }, + (table) => [ + index("backlink_snapshots_project_captured_idx").on( + table.projectId, + table.capturedAt, + ), + ], +); diff --git a/src/db/pg/app.schema.ts b/src/db/pg/app.schema.ts index dc5e046..f673cfd 100644 --- a/src/db/pg/app.schema.ts +++ b/src/db/pg/app.schema.ts @@ -343,3 +343,65 @@ export const rankSnapshots = pgTable( ), ], ); + +// Dashboard activation milestones. Organization-scoped: MCP OAuth grants are +// user-level, so any member connecting an external MCP client satisfies the +// milestone for the whole organization. Timestamps are first-occurrence only +// and never move once set. +export const organizationActivationState = pgTable( + "organization_activation_state", + { + organizationId: text("organization_id") + .primaryKey() + .references(() => organization.id, { onDelete: "cascade" }), + firstMcpAuthorizedAt: timestampColumn("first_mcp_authorized_at"), + firstMcpToolCallAt: timestampColumn("first_mcp_tool_call_at"), + updatedAt: timestampColumn("updated_at").notNull().default(isoNow), + }, +); + +// Per-project state for the dashboard's onboarding checklist. Most steps +// complete via real product state (projects.domain, gsc_connections, MCP +// activation); the competitor step completes on click-through. +export const projectActivationState = pgTable("project_activation_state", { + projectId: text("project_id") + .primaryKey() + .references(() => projects.id, { onDelete: "cascade" }), + competitorStepClickedAt: timestampColumn("competitor_step_clicked_at"), + // "I already connected" on the MCP card: hides the card for this project + // without faking the org-level first-tool-call milestone, which stays + // truthful and self-heals when a real external call lands. + mcpCardDismissedAt: timestampColumn("mcp_card_dismissed_at"), + updatedAt: timestampColumn("updated_at").notNull().default(isoNow), +}); + +// Point-in-time backlink profile summaries for the project's own domain, +// written by the dashboard's visit-triggered refresh. DataForSEO's summary +// already carries new/lost counts, so one snapshot renders a full card; +// rows accumulate into history for future trend views. The domain is stored +// per row so a later project-domain change doesn't rewrite history. +export const backlinkSnapshots = pgTable( + "backlink_snapshots", + { + id: serial("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + domain: text("domain").notNull(), + rank: integer("rank"), + backlinks: integer("backlinks"), + referringDomains: integer("referring_domains"), + brokenBacklinks: integer("broken_backlinks"), + newBacklinks: integer("new_backlinks"), + lostBacklinks: integer("lost_backlinks"), + newReferringDomains: integer("new_referring_domains"), + lostReferringDomains: integer("lost_referring_domains"), + capturedAt: timestampColumn("captured_at").notNull().default(isoNow), + }, + (table) => [ + index("backlink_snapshots_project_captured_idx").on( + table.projectId, + table.capturedAt, + ), + ], +); diff --git a/src/db/schema.ts b/src/db/schema.ts index b00c368..58af2a0 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -72,6 +72,9 @@ export const { rankTrackingKeywords, rankCheckRuns, rankSnapshots, + organizationActivationState, + projectActivationState, + backlinkSnapshots, audits, auditPages, auditLinks, diff --git a/src/routes/_app/index.tsx b/src/routes/_app/index.tsx index d4b0c74..e939fd8 100644 --- a/src/routes/_app/index.tsx +++ b/src/routes/_app/index.tsx @@ -40,7 +40,7 @@ function IndexRedirect() { } void navigate({ - to: "/p/$projectId/keywords", + to: "/p/$projectId", params: { projectId: (target ?? data[0]).id }, }); }, [data, navigate]); diff --git a/src/routes/_authenticated.onboarding.index.tsx b/src/routes/_authenticated.onboarding.index.tsx index 244cb3c..2ffee34 100644 --- a/src/routes/_authenticated.onboarding.index.tsx +++ b/src/routes/_authenticated.onboarding.index.tsx @@ -81,10 +81,7 @@ function OnboardingFlow({ const [answers, setAnswers] = useState(initialAnswers); const saveMutation = useMutation({ - mutationFn: (extra: { - mcpSetupIntent?: "yes" | "no"; - completed?: boolean; - }) => + mutationFn: (extra: { completed?: boolean }) => saveOnboardingAnswers({ data: buildOnboardingPayload(answers, step, extra), }), @@ -113,9 +110,9 @@ function OnboardingFlow({ goToStep(step + 1); }; - const handleFinish = async (mcpSetupIntent: "yes" | "no") => { + const handleFinish = async () => { try { - await saveMutation.mutateAsync({ mcpSetupIntent, completed: true }); + await saveMutation.mutateAsync({ completed: true }); // Refresh the shared cache so the destination's onboarding-redirect guard // sees the completed state and doesn't bounce the user back here. await queryClient.invalidateQueries({ queryKey: ["onboardingAnswers"] }); @@ -126,13 +123,9 @@ function OnboardingFlow({ interests: answers.selectedInterests, work_for: answers.workFor, source: answers.source, - wants_mcp_setup: mcpSetupIntent === "yes", }); - if (mcpSetupIntent === "yes") { - void navigate({ to: "/ai", replace: true }); - } else { - void navigate({ to: "/", replace: true }); - } + // The dashboard's onboarding checklist owns MCP coaching now. + void navigate({ to: "/", replace: true }); }; return ( diff --git a/src/routes/_project/p/$projectId/index.tsx b/src/routes/_project/p/$projectId/index.tsx index 8a24560..368671c 100644 --- a/src/routes/_project/p/$projectId/index.tsx +++ b/src/routes/_project/p/$projectId/index.tsx @@ -1,10 +1,11 @@ -import { createFileRoute, redirect } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; +import { DashboardPage } from "@/client/features/dashboard/DashboardPage"; export const Route = createFileRoute("/_project/p/$projectId/")({ - beforeLoad: ({ params }) => { - throw redirect({ - to: "/p/$projectId/keywords", - params: { projectId: params.projectId }, - }); - }, + component: DashboardRoute, }); + +function DashboardRoute() { + const { projectId } = Route.useParams(); + return ; +} diff --git a/src/server/features/activation/mcpActivation.ts b/src/server/features/activation/mcpActivation.ts new file mode 100644 index 0000000..097b977 --- /dev/null +++ b/src/server/features/activation/mcpActivation.ts @@ -0,0 +1,36 @@ +import { ActivationRepository } from "@/server/features/activation/repositories/ActivationRepository"; + +// Orgs whose first external MCP tool call is already recorded (or in flight) +// in this isolate. Only *first* timestamps matter, so after one successful +// write the tool-call hot path never touches the DB again for that org. +const recordedToolCallOrgs = new Set(); + +/** + * Milestone writes for the dashboard's MCP activation card. Both are awaited + * inline by their callers (not waitUntil) so the upsert runs inside the + * request's DB client scope, and both swallow errors: activation tracking + * must never fail an OAuth flow or a tool call. + */ +export async function recordMcpAuthorized( + organizationId: string, +): Promise { + try { + await ActivationRepository.recordFirstMcpAuthorized(organizationId); + } catch (error) { + console.error("activation: recordMcpAuthorized failed", error); + } +} + +export async function recordExternalMcpToolCall( + organizationId: string, +): Promise { + if (recordedToolCallOrgs.has(organizationId)) return; + recordedToolCallOrgs.add(organizationId); + try { + await ActivationRepository.recordFirstMcpToolCall(organizationId); + } catch (error) { + // Allow a retry on a later call rather than losing the milestone. + recordedToolCallOrgs.delete(organizationId); + console.error("activation: recordExternalMcpToolCall failed", error); + } +} diff --git a/src/server/features/activation/repositories/ActivationRepository.ts b/src/server/features/activation/repositories/ActivationRepository.ts new file mode 100644 index 0000000..870b2a9 --- /dev/null +++ b/src/server/features/activation/repositories/ActivationRepository.ts @@ -0,0 +1,99 @@ +import { eq, sql } from "drizzle-orm"; +import { db } from "@/db"; +import { + organizationActivationState, + projectActivationState, +} from "@/db/schema"; + +type OrganizationActivationState = + typeof organizationActivationState.$inferSelect; +type ProjectActivationState = typeof projectActivationState.$inferSelect; + +async function getOrganizationActivation( + organizationId: string, +): Promise { + const rows = await db + .select() + .from(organizationActivationState) + .where(eq(organizationActivationState.organizationId, organizationId)) + .limit(1); + return rows[0] ?? null; +} + +async function getProjectActivation( + projectId: string, +): Promise { + const rows = await db + .select() + .from(projectActivationState) + .where(eq(projectActivationState.projectId, projectId)) + .limit(1); + return rows[0] ?? null; +} + +// First-occurrence timestamps only: concurrent writers race harmlessly because +// COALESCE keeps whichever value landed first. +async function recordFirstMcpAuthorized(organizationId: string): Promise { + const now = new Date().toISOString(); + await db + .insert(organizationActivationState) + .values({ organizationId, firstMcpAuthorizedAt: now, updatedAt: now }) + .onConflictDoUpdate({ + target: organizationActivationState.organizationId, + set: { + firstMcpAuthorizedAt: sql`coalesce(${organizationActivationState.firstMcpAuthorizedAt}, ${now})`, + updatedAt: now, + }, + }); +} + +async function recordFirstMcpToolCall(organizationId: string): Promise { + const now = new Date().toISOString(); + await db + .insert(organizationActivationState) + .values({ organizationId, firstMcpToolCallAt: now, updatedAt: now }) + .onConflictDoUpdate({ + target: organizationActivationState.organizationId, + set: { + firstMcpToolCallAt: sql`coalesce(${organizationActivationState.firstMcpToolCallAt}, ${now})`, + updatedAt: now, + }, + }); +} + +async function markCompetitorStepClicked(projectId: string): Promise { + const now = new Date().toISOString(); + await db + .insert(projectActivationState) + .values({ projectId, competitorStepClickedAt: now, updatedAt: now }) + .onConflictDoUpdate({ + target: projectActivationState.projectId, + set: { + competitorStepClickedAt: sql`coalesce(${projectActivationState.competitorStepClickedAt}, ${now})`, + updatedAt: now, + }, + }); +} + +async function markMcpCardDismissed(projectId: string): Promise { + const now = new Date().toISOString(); + await db + .insert(projectActivationState) + .values({ projectId, mcpCardDismissedAt: now, updatedAt: now }) + .onConflictDoUpdate({ + target: projectActivationState.projectId, + set: { + mcpCardDismissedAt: sql`coalesce(${projectActivationState.mcpCardDismissedAt}, ${now})`, + updatedAt: now, + }, + }); +} + +export const ActivationRepository = { + getOrganizationActivation, + getProjectActivation, + recordFirstMcpAuthorized, + recordFirstMcpToolCall, + markCompetitorStepClicked, + markMcpCardDismissed, +}; diff --git a/src/server/features/audit/repositories/auditSummaryQueries.ts b/src/server/features/audit/repositories/auditSummaryQueries.ts new file mode 100644 index 0000000..6ac6bb9 --- /dev/null +++ b/src/server/features/audit/repositories/auditSummaryQueries.ts @@ -0,0 +1,21 @@ +import { countDistinct, eq } from "drizzle-orm"; +import { db } from "@/db"; +import { auditIssues } from "@/db/schema"; + +/** + * Distinct-page counts per issue type for one audit — link-level issues + * write one row per occurrence, and consumers phrase this as "N pages". + * Lives beside AuditRepository (same pattern as rank-tracking's + * snapshotQueries) to keep the main repository under the file-size limit. + */ +export async function getIssueTypePageCountsForAudit(auditId: string) { + return db + .select({ + issueType: auditIssues.issueType, + severity: auditIssues.severity, + pages: countDistinct(auditIssues.pageUrl), + }) + .from(auditIssues) + .where(eq(auditIssues.auditId, auditId)) + .groupBy(auditIssues.issueType, auditIssues.severity); +} diff --git a/src/server/features/dashboard/repositories/BacklinkSnapshotRepository.ts b/src/server/features/dashboard/repositories/BacklinkSnapshotRepository.ts new file mode 100644 index 0000000..977129e --- /dev/null +++ b/src/server/features/dashboard/repositories/BacklinkSnapshotRepository.ts @@ -0,0 +1,34 @@ +import { desc, eq } from "drizzle-orm"; +import { db } from "@/db"; +import { backlinkSnapshots } from "@/db/schema"; + +type BacklinkSnapshot = typeof backlinkSnapshots.$inferSelect; + +async function getLatestForProject( + projectId: string, +): Promise { + const rows = await db + .select() + .from(backlinkSnapshots) + .where(eq(backlinkSnapshots.projectId, projectId)) + // id, not capturedAt: autoincrement is monotonic and immune to the + // sqlite-vs-pg timestamp text-format difference. + .orderBy(desc(backlinkSnapshots.id)) + .limit(1); + return rows[0] ?? null; +} + +async function insert( + values: typeof backlinkSnapshots.$inferInsert, +): Promise { + const [row] = await db.insert(backlinkSnapshots).values(values).returning(); + if (!row) { + throw new Error("Failed to insert backlink_snapshot"); + } + return row; +} + +export const BacklinkSnapshotRepository = { + getLatestForProject, + insert, +}; diff --git a/src/server/features/dashboard/services/DashboardService.ts b/src/server/features/dashboard/services/DashboardService.ts new file mode 100644 index 0000000..cecbb99 --- /dev/null +++ b/src/server/features/dashboard/services/DashboardService.ts @@ -0,0 +1,273 @@ +import type { BillingCustomerContext } from "@/server/billing/subscription"; +import { ActivationRepository } from "@/server/features/activation/repositories/ActivationRepository"; +import { AuditRepository } from "@/server/features/audit/repositories/AuditRepository"; +import { getIssueTypePageCountsForAudit } from "@/server/features/audit/repositories/auditSummaryQueries"; +import { BacklinkSnapshotRepository } from "@/server/features/dashboard/repositories/BacklinkSnapshotRepository"; +import { GscConnectionRepository } from "@/server/features/gsc/repositories/GscConnectionRepository"; +import { RankTrackingRepository } from "@/server/features/rank-tracking/repositories/RankTrackingRepository"; +import { getLatestResults } from "@/server/features/rank-tracking/services/rankTrackingResults"; +import { + createDataforseoClient, + normalizeBacklinksTarget, +} from "@/server/lib/dataforseo"; + +// Daily cadence: fresh numbers each visit without per-visit spend; a dormant +// project costs nothing because refreshes are visit-triggered. +const SNAPSHOT_MAX_AGE_MS = 24 * 60 * 60 * 1000; +// Bounds the per-config result reads on the overview path; projects rarely +// have more than a couple of configs. +const MAX_CONFIGS_FOR_OVERVIEW = 5; + +export type DashboardActivation = { + domain: string | null; + gsc: { connected: boolean; siteUrl: string | null }; + mcp: { + authorizedAt: string | null; + firstToolCallAt: string | null; + cardDismissedAt: string | null; + }; + competitorClickedAt: string | null; +}; + +export type DashboardRankSummary = { + trackedKeywords: number; + improved: number; + declined: number; + top10: number; + lastCheckedAt: string | null; +}; + +export type DashboardAuditSummary = { + status: "running" | "completed" | "failed"; + pagesCrawled: number; + startedAt: string; + // Top issue types by severity then affected-page count, for the card's list. + topIssues: { + issueType: string; + severity: "critical" | "warning" | "info"; + count: number; + }[]; + totalIssueTypes: number; +}; + +export type DashboardBacklinkSummary = { + domain: string; + rank: number | null; + backlinks: number | null; + referringDomains: number | null; + newBacklinks: number | null; + lostBacklinks: number | null; + newReferringDomains: number | null; + lostReferringDomains: number | null; + capturedAt: string; + stale: boolean; +}; + +type DashboardOverview = { + rank: DashboardRankSummary | null; + audit: DashboardAuditSummary | null; + backlinks: DashboardBacklinkSummary | null; +}; + +async function getActivation(input: { + projectId: string; + organizationId: string; + domain: string | null; +}): Promise { + const [gsc, orgActivation, projectActivation] = await Promise.all([ + GscConnectionRepository.getByProjectId(input.projectId), + ActivationRepository.getOrganizationActivation(input.organizationId), + ActivationRepository.getProjectActivation(input.projectId), + ]); + + return { + domain: input.domain, + gsc: { connected: gsc !== null, siteUrl: gsc?.siteUrl ?? null }, + mcp: { + authorizedAt: orgActivation?.firstMcpAuthorizedAt ?? null, + firstToolCallAt: orgActivation?.firstMcpToolCallAt ?? null, + cardDismissedAt: projectActivation?.mcpCardDismissedAt ?? null, + }, + competitorClickedAt: projectActivation?.competitorStepClickedAt ?? null, + }; +} + +async function getOverview(input: { + projectId: string; + domain: string | null; +}): Promise { + const [rank, audit, backlinks] = await Promise.all([ + getRankSummary(input.projectId), + getAuditSummary(input.projectId), + getBacklinkSummary(input.projectId, input.domain), + ]); + return { rank, audit, backlinks }; +} + +async function getRankSummary( + projectId: string, +): Promise { + const configs = await RankTrackingRepository.getConfigsForProject(projectId); + if (configs.length === 0) return null; + + const results = await Promise.all( + configs + .slice(0, MAX_CONFIGS_FOR_OVERVIEW) + .map((config) => getLatestResults(config.id, projectId, "7d")), + ); + + const summary: DashboardRankSummary = { + trackedKeywords: 0, + improved: 0, + declined: 0, + top10: 0, + lastCheckedAt: null, + }; + + for (const result of results) { + summary.trackedKeywords += result.rows.length; + if ( + result.run && + (!summary.lastCheckedAt || + result.run.lastCheckedAt > summary.lastCheckedAt) + ) { + summary.lastCheckedAt = result.run.lastCheckedAt; + } + for (const row of result.rows) { + for (const device of ["desktop", "mobile"] as const) { + const { position, previousPosition } = row[device]; + if (position !== null && position <= 10) summary.top10 += 1; + if (position === null || previousPosition === null) continue; + // Lower position number = better ranking. + if (position < previousPosition) summary.improved += 1; + else if (position > previousPosition) summary.declined += 1; + } + } + } + + return summary; +} + +async function getAuditSummary( + projectId: string, +): Promise { + const audit = await AuditRepository.getLatestAuditForProject(projectId); + if (!audit) return null; + + const typeRows = await getIssueTypePageCountsForAudit(audit.id); + + const severityRank = { critical: 0, warning: 1, info: 2 }; + const sorted = typeRows + .map((row) => ({ + issueType: row.issueType, + severity: row.severity, + count: row.pages, + })) + .toSorted( + (a, b) => + severityRank[a.severity] - severityRank[b.severity] || + b.count - a.count, + ); + + return { + status: audit.status, + pagesCrawled: audit.pagesCrawled, + startedAt: audit.startedAt, + topIssues: sorted.slice(0, 3), + totalIssueTypes: sorted.length, + }; +} + +function isSnapshotFresh(capturedAt: string): boolean { + const capturedMs = Date.parse(capturedAt); + if (Number.isNaN(capturedMs)) return false; + return Date.now() - capturedMs < SNAPSHOT_MAX_AGE_MS; +} + +async function getBacklinkSummary( + projectId: string, + domain: string | null, +): Promise { + if (!domain) return null; + const snapshot = + await BacklinkSnapshotRepository.getLatestForProject(projectId); + if (!snapshot || snapshot.domain !== domain) return null; + return { + domain: snapshot.domain, + rank: snapshot.rank, + backlinks: snapshot.backlinks, + referringDomains: snapshot.referringDomains, + newBacklinks: snapshot.newBacklinks, + lostBacklinks: snapshot.lostBacklinks, + newReferringDomains: snapshot.newReferringDomains, + lostReferringDomains: snapshot.lostReferringDomains, + capturedAt: snapshot.capturedAt, + stale: !isSnapshotFresh(snapshot.capturedAt), + }; +} + +/** + * Visit-triggered snapshot refresh. Fetches only the DataForSEO backlinks + * summary (not the history endpoint the backlinks page also pays for) and is + * a no-op while the latest snapshot for the current domain is under a day + * old. Concurrent loads racing the freshness check can each pay a metered + * call — every call is metered, so the race duplicates customer spend on + * identical data but never leaks revenue; accepted for now. On a fetch + * failure with a stale snapshot in hand, the stale snapshot is returned + * rather than surfacing an error card. + */ +async function ensureBacklinkSnapshot(input: { + projectId: string; + domain: string | null; + billingCustomer: BillingCustomerContext; +}): Promise { + const { projectId, domain } = input; + if (!domain) return null; + + const latest = + await BacklinkSnapshotRepository.getLatestForProject(projectId); + const latestMatchesDomain = latest !== null && latest.domain === domain; + if (latest && latestMatchesDomain && isSnapshotFresh(latest.capturedAt)) { + return getBacklinkSummary(projectId, domain); + } + + const normalized = normalizeBacklinksTarget(domain, { scope: "domain" }); + const dataforseo = createDataforseoClient(input.billingCustomer); + + try { + const summary = await dataforseo.backlinks.summary({ + target: normalized.apiTarget, + }); + await BacklinkSnapshotRepository.insert({ + projectId, + domain, + rank: summary.rank ?? null, + backlinks: summary.backlinks ?? null, + referringDomains: summary.referring_domains ?? null, + brokenBacklinks: summary.broken_backlinks ?? null, + newBacklinks: summary.new_backlinks ?? null, + lostBacklinks: summary.lost_backlinks ?? null, + newReferringDomains: + summary.new_referring_domains ?? summary.new_reffering_domains ?? null, + lostReferringDomains: + summary.lost_referring_domains ?? + summary.lost_reffering_domains ?? + null, + capturedAt: new Date().toISOString(), + }); + } catch (error) { + if (latestMatchesDomain) { + console.error("dashboard: backlink snapshot refresh failed", error); + return getBacklinkSummary(projectId, domain); + } + throw error; + } + + return getBacklinkSummary(projectId, domain); +} + +export const DashboardService = { + getActivation, + getOverview, + ensureBacklinkSnapshot, +}; diff --git a/src/server/features/projects/repositories/ProjectRepository.ts b/src/server/features/projects/repositories/ProjectRepository.ts index f76b7b9..ab05869 100644 --- a/src/server/features/projects/repositories/ProjectRepository.ts +++ b/src/server/features/projects/repositories/ProjectRepository.ts @@ -101,6 +101,31 @@ async function updateProject( return row; } +// Writes only the domain column, for the dashboard's inline domain input. +async function updateProjectDomain( + projectId: string, + organizationId: string, + domain: string, +) { + const [row] = await db + .update(projects) + .set({ domain }) + .where( + and( + eq(projects.id, projectId), + eq(projects.organizationId, organizationId), + isNull(projects.archivedAt), + ), + ) + .returning(); + + if (!row) { + throw new AppError("NOT_FOUND"); + } + + return row; +} + // Writes only the market columns. Onboarding sets the project's market before // the user has named the project or picked a domain, so it must not go through // updateProject, whose `domain: input.domain ?? null` would clear the domain. @@ -197,6 +222,7 @@ export const ProjectRepository = { getProjectById, createProject, updateProject, + updateProjectDomain, updateProjectMarket, tryCreateDefaultProject, archiveProject, diff --git a/src/server/features/projects/services/ProjectService.ts b/src/server/features/projects/services/ProjectService.ts index 5489f4c..e34f44f 100644 --- a/src/server/features/projects/services/ProjectService.ts +++ b/src/server/features/projects/services/ProjectService.ts @@ -6,6 +6,7 @@ import { listProjects, listProjectsEnsuringOne, restoreProject, + setProjectDomain, setProjectMarket, updateProject, } from "@/server/features/projects/services/projects"; @@ -15,6 +16,7 @@ export const ProjectService = { listProjectsEnsuringOne, createProject, updateProject, + setProjectDomain, setProjectMarket, archiveProject, restoreProject, diff --git a/src/server/features/projects/services/projects.test.ts b/src/server/features/projects/services/projects.test.ts index ac8b460..745ce96 100644 --- a/src/server/features/projects/services/projects.test.ts +++ b/src/server/features/projects/services/projects.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ createProject: vi.fn(), updateProject: vi.fn(), + updateProjectDomain: vi.fn(), archiveProject: vi.fn(), restoreProject: vi.fn(), countProjects: vi.fn(), @@ -208,6 +209,51 @@ describe("project service", () => { }); }); + describe("setProjectDomain", () => { + it("canonicalizes a pasted URL to the bare host before writing", async () => { + mocks.updateProjectDomain.mockResolvedValue(namedProject); + const { setProjectDomain } = await import("./projects"); + + await setProjectDomain("org_1", { + projectId: "project_acme", + domain: "https://www.Acme.com/pricing?ref=x", + }); + + expect(mocks.updateProjectDomain).toHaveBeenCalledWith( + "project_acme", + "org_1", + "acme.com", + ); + }); + + it("rejects junk that the backlink fetch would later refuse", async () => { + const { setProjectDomain } = await import("./projects"); + + await expect( + setProjectDomain("org_1", { + projectId: "project_acme", + domain: "not a domain", + }), + ).rejects.toThrow("Enter a valid domain"); + expect(mocks.updateProjectDomain).not.toHaveBeenCalled(); + }); + }); + + describe("updateProject domain validation", () => { + it("rejects a junk domain instead of storing it", async () => { + const { updateProject } = await import("./projects"); + + await expect( + updateProject("org_1", { + projectId: "project_acme", + name: "Acme", + domain: "999.999.999.999", + }), + ).rejects.toThrow("Enter a valid domain"); + expect(mocks.updateProject).not.toHaveBeenCalled(); + }); + }); + describe("setProjectMarket", () => { it("writes only the market columns, leaving name and domain untouched", async () => { // Onboarding sets the market before the project is named or given a diff --git a/src/server/features/projects/services/projects.ts b/src/server/features/projects/services/projects.ts index dcfd8b8..27d0209 100644 --- a/src/server/features/projects/services/projects.ts +++ b/src/server/features/projects/services/projects.ts @@ -2,10 +2,12 @@ import type { ArchiveProjectInput, CreateProjectInput, RestoreProjectInput, + SetProjectDomainInput, SetProjectMarketInput, UpdateProjectInput, } from "@/types/schemas/projects"; import { ProjectRepository } from "@/server/features/projects/repositories/ProjectRepository"; +import { normalizeBacklinksTarget } from "@/server/lib/dataforseoBacklinksTarget"; import { AppError } from "@/server/lib/errors"; import { assertLanguageForLocation } from "@/server/lib/market"; import { getLanguageCode } from "@/shared/keyword-locations"; @@ -83,6 +85,24 @@ export async function listProjectsEnsuringOne(organizationId: string) { return listProjects(organizationId); } +/** + * Validates and canonicalizes a project domain (lowercase bare host, www and + * protocol/path stripped) with the same rules the backlink fetch will apply + * later, so junk fails at save time instead of at the first paid call. + * Undefined passes through — updateProject uses that to clear the domain. + */ +function normalizeProjectDomain(domain: string | undefined) { + if (domain === undefined) return undefined; + try { + return normalizeBacklinksTarget(domain, { scope: "domain" }).apiTarget; + } catch { + throw new AppError( + "VALIDATION_ERROR", + "Enter a valid domain, like acme.com.", + ); + } +} + export async function createProject( organizationId: string, input: CreateProjectInput, @@ -91,7 +111,7 @@ export async function createProject( const row = await ProjectRepository.createProject( organizationId, input.name, - input.domain, + normalizeProjectDomain(input.domain), resolveMarketInput(input), ); return mapProject(row); @@ -113,7 +133,7 @@ export async function updateProject( organizationId, { name: input.name, - domain: input.domain, + domain: normalizeProjectDomain(input.domain), market: resolveMarketInput(input), }, ); @@ -126,6 +146,27 @@ export async function updateProject( } } +/** + * Sets a project's domain on its own, for the dashboard hero's inline input. + * Writing just this column keeps the write from echoing a name/market the + * caller never edited. + */ +export async function setProjectDomain( + organizationId: string, + input: SetProjectDomainInput, +) { + const domain = normalizeProjectDomain(input.domain); + if (domain === undefined) { + throw new AppError("VALIDATION_ERROR", "Enter a valid domain."); + } + const row = await ProjectRepository.updateProjectDomain( + input.projectId, + organizationId, + domain, + ); + return mapProject(row); +} + /** * Sets a project's default market on its own, for surfaces that only ask for * the market (onboarding). Writing just these two columns keeps the write from diff --git a/src/server/mcp/instrumentation.test.ts b/src/server/mcp/instrumentation.test.ts index 463d290..bcf0881 100644 --- a/src/server/mcp/instrumentation.test.ts +++ b/src/server/mcp/instrumentation.test.ts @@ -11,6 +11,7 @@ import { AppError } from "@/server/lib/errors"; const mocks = vi.hoisted(() => ({ captureServerError: vi.fn(), captureServerEvent: vi.fn(), + recordExternalMcpToolCall: vi.fn(), incrementSelfHostMcpToolCallCount: vi.fn(), })); @@ -24,6 +25,12 @@ vi.mock("@/server/lib/posthog", () => ({ captureServerEvent: mocks.captureServerEvent, })); +// The real module pulls in @/db (cloudflare:workers env) — mock it out and +// assert the milestone hook at this boundary instead. +vi.mock("@/server/features/activation/mcpActivation", () => ({ + recordExternalMcpToolCall: mocks.recordExternalMcpToolCall, +})); + vi.mock("@/server/lib/self-host-telemetry", () => ({ incrementSelfHostMcpToolCallCount: mocks.incrementSelfHostMcpToolCallCount, })); @@ -56,6 +63,7 @@ describe("instrumentMcpToolHandler", () => { beforeEach(() => { mocks.captureServerError.mockReset(); mocks.captureServerEvent.mockReset(); + mocks.recordExternalMcpToolCall.mockReset(); mocks.incrementSelfHostMcpToolCallCount.mockReset(); }); @@ -172,5 +180,45 @@ describe("instrumentMcpToolHandler", () => { await wrapped({}, toolExtra); expect(mocks.captureServerEvent).not.toHaveBeenCalled(); + expect(mocks.recordExternalMcpToolCall).not.toHaveBeenCalled(); + }); + + it("records the activation milestone for a successful external call", async () => { + const { instrumentMcpToolHandler } = await import("./instrumentation"); + const wrapped = instrumentMcpToolHandler("demo", outputSchema, async () => + okResult({ items: [] }), + ); + + await runWithMcpToolAuthContext(authContext, () => wrapped({}, toolExtra)); + + expect(mocks.recordExternalMcpToolCall).toHaveBeenCalledExactlyOnceWith( + "org-1", + ); + }); + + it("skips the activation milestone for first-party (null clientId) calls", async () => { + const { instrumentMcpToolHandler } = await import("./instrumentation"); + const wrapped = instrumentMcpToolHandler("demo", outputSchema, async () => + okResult({ items: [] }), + ); + + await runWithMcpToolAuthContext({ ...authContext, clientId: null }, () => + wrapped({}, toolExtra), + ); + + expect(mocks.recordExternalMcpToolCall).not.toHaveBeenCalled(); + }); + + it("skips the activation milestone when the call fails", async () => { + const { instrumentMcpToolHandler } = await import("./instrumentation"); + const wrapped = instrumentMcpToolHandler("demo", outputSchema, async () => { + throw new AppError("NOT_FOUND"); + }); + + await expect( + runWithMcpToolAuthContext(authContext, () => wrapped({}, toolExtra)), + ).rejects.toThrow("NOT_FOUND"); + + expect(mocks.recordExternalMcpToolCall).not.toHaveBeenCalled(); }); }); diff --git a/src/server/mcp/instrumentation.ts b/src/server/mcp/instrumentation.ts index 9df401a..c297f08 100644 --- a/src/server/mcp/instrumentation.ts +++ b/src/server/mcp/instrumentation.ts @@ -8,6 +8,7 @@ import { type ZodRawShapeCompat, } from "@modelcontextprotocol/sdk/server/zod-compat.js"; import { asAppError } from "@/server/lib/errors"; +import { recordExternalMcpToolCall } from "@/server/features/activation/mcpActivation"; import { captureServerError, captureServerEvent } from "@/server/lib/posthog"; import { shouldCaptureAppErrorCode } from "@/shared/error-codes"; import { getAuth, type ToolExtra } from "@/server/mcp/context"; @@ -114,6 +115,21 @@ export function instrumentMcpToolHandler( ? { success: false, errorCode: "MCP_OUTPUT_VALIDATION" } : { success: !result.isError }, ); + // Dashboard activation milestone: a successful call from an external + // MCP client (OAuth clientId; SAM and the self-hosted transport are + // first-party with clientId null). Awaited so the write stays inside + // the request's DB scope; a per-isolate memo keeps this off the hot + // path after the first call. + if (!result.isError && !outputValidationFailed) { + try { + const auth = getAuth(extra); + if (auth.clientId) { + await recordExternalMcpToolCall(auth.organizationId); + } + } catch { + // no auth context — skip milestone tracking + } + } return result; } catch (error) { const appError = asAppError(error); diff --git a/src/server/mcp/oauth-provider.ts b/src/server/mcp/oauth-provider.ts index 9c35fb3..5026c58 100644 --- a/src/server/mcp/oauth-provider.ts +++ b/src/server/mcp/oauth-provider.ts @@ -13,6 +13,7 @@ import { MCP_SCOPE, } from "@/lib/oauth-resource"; import { asAppError } from "@/server/lib/errors"; +import { recordMcpAuthorized } from "@/server/features/activation/mcpActivation"; import { captureServerEvent } from "@/server/lib/posthog"; import { createWorkersOAuthMcpProps, @@ -366,6 +367,8 @@ async function handleOAuthConsentResponse( props, }); + await recordMcpAuthorized(context.organizationId); + waitUntil( captureServerEvent({ distinctId: context.userId, diff --git a/src/serverFunctions/dashboard.ts b/src/serverFunctions/dashboard.ts new file mode 100644 index 0000000..ef9b5cf --- /dev/null +++ b/src/serverFunctions/dashboard.ts @@ -0,0 +1,63 @@ +import { createServerFn } from "@tanstack/react-start"; +import { ActivationRepository } from "@/server/features/activation/repositories/ActivationRepository"; +import { DashboardService } from "@/server/features/dashboard/services/DashboardService"; +import { requireProjectContext } from "@/serverFunctions/middleware"; +import { dashboardProjectInputSchema } from "@/types/schemas/dashboard"; + +export const getDashboardActivation = createServerFn({ method: "POST" }) + .middleware(requireProjectContext) + .validator(dashboardProjectInputSchema) + .handler(({ context }) => + DashboardService.getActivation({ + projectId: context.projectId, + organizationId: context.organizationId, + domain: context.project.domain, + }), + ); + +export const getDashboardOverview = createServerFn({ method: "POST" }) + .middleware(requireProjectContext) + .validator(dashboardProjectInputSchema) + .handler(({ context }) => + DashboardService.getOverview({ + projectId: context.projectId, + domain: context.project.domain, + }), + ); + +// Visit-triggered: the client calls this when the overview reports a missing +// or stale backlink snapshot. Metered against org credits at most once per +// project per day (the service re-checks freshness server-side). +export const refreshDashboardBacklinkSnapshot = createServerFn({ + method: "POST", +}) + .middleware(requireProjectContext) + .validator(dashboardProjectInputSchema) + .handler(({ context }) => + DashboardService.ensureBacklinkSnapshot({ + projectId: context.projectId, + domain: context.project.domain, + billingCustomer: context, + }), + ); + +export const markDashboardCompetitorClicked = createServerFn({ + method: "POST", +}) + .middleware(requireProjectContext) + .validator(dashboardProjectInputSchema) + .handler(async ({ context }) => { + await ActivationRepository.markCompetitorStepClicked(context.projectId); + return { ok: true as const }; + }); + +// "I already connected" on the MCP card. Hides the card for this project; +// the org-level milestone stays untouched and self-corrects on the next +// real external tool call. +export const dismissDashboardMcpCard = createServerFn({ method: "POST" }) + .middleware(requireProjectContext) + .validator(dashboardProjectInputSchema) + .handler(async ({ context }) => { + await ActivationRepository.markMcpCardDismissed(context.projectId); + return { ok: true as const }; + }); diff --git a/src/serverFunctions/projects.ts b/src/serverFunctions/projects.ts index a8da794..90378cc 100644 --- a/src/serverFunctions/projects.ts +++ b/src/serverFunctions/projects.ts @@ -8,6 +8,7 @@ import { archiveProjectSchema, createProjectSchema, restoreProjectSchema, + setProjectDomainSchema, setProjectMarketSchema, updateProjectSchema, } from "@/types/schemas/projects"; @@ -35,6 +36,13 @@ export const updateProject = createServerFn({ method: "POST" }) ProjectService.updateProject(context.organizationId, data), ); +export const setProjectDomain = createServerFn({ method: "POST" }) + .middleware(requireProjectContext) + .validator(setProjectDomainSchema) + .handler(async ({ data, context }) => + ProjectService.setProjectDomain(context.organizationId, data), + ); + export const setProjectMarket = createServerFn({ method: "POST" }) .middleware(requireProjectContext) .validator(setProjectMarketSchema) diff --git a/src/types/schemas/dashboard.ts b/src/types/schemas/dashboard.ts new file mode 100644 index 0000000..29b7690 --- /dev/null +++ b/src/types/schemas/dashboard.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const dashboardHeroStepSchema = z.enum([ + "domain", + "mcp", + "gsc", + "competitor", +]); +export type DashboardHeroStep = z.infer; + +export const dashboardProjectInputSchema = z.object({ + projectId: z.string().min(1), +}); diff --git a/src/types/schemas/projects.ts b/src/types/schemas/projects.ts index 98180a1..31a29b8 100644 --- a/src/types/schemas/projects.ts +++ b/src/types/schemas/projects.ts @@ -63,6 +63,13 @@ export const updateProjectSchema = z }) .refine(hasLocationForLanguage, marketPairMessage); +// Domain on its own, for the dashboard hero's inline input. Same loose shape +// as updateProjectSchema's domain field, but required. +export const setProjectDomainSchema = z.object({ + projectId: z.string().min(1), + domain: z.string().trim().min(1).max(255), +}); + // Market-only update (onboarding). Both halves are required: the caller picks // them together, so the service can validate the pair without a stored row. export const setProjectMarketSchema = z.object({ @@ -89,6 +96,7 @@ export const restoreProjectSchema = z.object({ export type CreateProjectInput = z.infer; export type UpdateProjectInput = z.infer; +export type SetProjectDomainInput = z.infer; export type SetProjectMarketInput = z.infer; export type ArchiveProjectInput = z.infer; export type RestoreProjectInput = z.infer;