diff --git a/apps/web/drizzle/0063_unique_moondragon.sql b/apps/web/drizzle/0063_unique_moondragon.sql new file mode 100644 index 00000000..33f301e8 --- /dev/null +++ b/apps/web/drizzle/0063_unique_moondragon.sql @@ -0,0 +1,13 @@ +-- Custom SQL migration file, put your code below! -- +CREATE TABLE `scanner_settings` ( + `user_id` text PRIMARY KEY NOT NULL, + `preprocessing` text DEFAULT 'multi' NOT NULL, + `enable_histogram_equalization` integer DEFAULT true NOT NULL, + `enable_adaptive_threshold` integer DEFAULT true NOT NULL, + `enable_morph_gradient` integer DEFAULT true NOT NULL, + `canny_low` integer DEFAULT 50 NOT NULL, + `canny_high` integer DEFAULT 150 NOT NULL, + `adaptive_block_size` integer DEFAULT 11 NOT NULL, + `adaptive_c` real DEFAULT 2 NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/apps/web/drizzle/0064_thin_marvel_apes.sql b/apps/web/drizzle/0064_thin_marvel_apes.sql new file mode 100644 index 00000000..30322bd4 --- /dev/null +++ b/apps/web/drizzle/0064_thin_marvel_apes.sql @@ -0,0 +1,2 @@ +-- Custom SQL migration file, put your code below! -- +ALTER TABLE `scanner_settings` ADD `enable_hough_lines` integer DEFAULT true NOT NULL; diff --git a/apps/web/drizzle/meta/0062_snapshot.json b/apps/web/drizzle/meta/0062_snapshot.json index 51d5c287..7db894ad 100644 --- a/apps/web/drizzle/meta/0062_snapshot.json +++ b/apps/web/drizzle/meta/0062_snapshot.json @@ -116,13 +116,9 @@ "abacus_settings_user_id_users_id_fk": { "name": "abacus_settings_user_id_users_id_fk", "tableFrom": "abacus_settings", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -240,9 +236,7 @@ "indexes": { "arcade_rooms_code_unique": { "name": "arcade_rooms_code_unique", - "columns": [ - "code" - ], + "columns": ["code"], "isUnique": true } }, @@ -339,26 +333,18 @@ "arcade_sessions_room_id_arcade_rooms_id_fk": { "name": "arcade_sessions_room_id_arcade_rooms_id_fk", "tableFrom": "arcade_sessions", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "arcade_rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "arcade_sessions_user_id_users_id_fk": { "name": "arcade_sessions_user_id_users_id_fk", "tableFrom": "arcade_sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -424,9 +410,7 @@ "indexes": { "players_user_id_idx": { "name": "players_user_id_idx", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "isUnique": false } }, @@ -434,13 +418,9 @@ "players_user_id_users_id_fk": { "name": "players_user_id_users_id_fk", "tableFrom": "players", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -514,9 +494,7 @@ "indexes": { "idx_room_members_user_id_unique": { "name": "idx_room_members_user_id_unique", - "columns": [ - "user_id" - ], + "columns": ["user_id"], "isUnique": true } }, @@ -524,13 +502,9 @@ "room_members_room_id_arcade_rooms_id_fk": { "name": "room_members_room_id_arcade_rooms_id_fk", "tableFrom": "room_members", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "arcade_rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -605,13 +579,9 @@ "room_member_history_room_id_arcade_rooms_id_fk": { "name": "room_member_history_room_id_arcade_rooms_id_fk", "tableFrom": "room_member_history", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "arcade_rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -713,10 +683,7 @@ "indexes": { "idx_room_invitations_user_room": { "name": "idx_room_invitations_user_room", - "columns": [ - "user_id", - "room_id" - ], + "columns": ["user_id", "room_id"], "isUnique": true } }, @@ -724,13 +691,9 @@ "room_invitations_room_id_arcade_rooms_id_fk": { "name": "room_invitations_room_id_arcade_rooms_id_fk", "tableFrom": "room_invitations", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "arcade_rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -833,13 +796,9 @@ "room_reports_room_id_arcade_rooms_id_fk": { "name": "room_reports_room_id_arcade_rooms_id_fk", "tableFrom": "room_reports", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "arcade_rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -918,10 +877,7 @@ "indexes": { "idx_room_bans_user_room": { "name": "idx_room_bans_user_room", - "columns": [ - "user_id", - "room_id" - ], + "columns": ["user_id", "room_id"], "isUnique": true } }, @@ -929,13 +885,9 @@ "room_bans_room_id_arcade_rooms_id_fk": { "name": "room_bans_room_id_arcade_rooms_id_fk", "tableFrom": "room_bans", - "columnsFrom": [ - "room_id" - ], + "columnsFrom": ["room_id"], "tableTo": "arcade_rooms", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -998,13 +950,9 @@ "user_stats_user_id_users_id_fk": { "name": "user_stats_user_id_users_id_fk", "tableFrom": "user_stats", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1062,16 +1010,12 @@ "indexes": { "users_guest_id_unique": { "name": "users_guest_id_unique", - "columns": [ - "guest_id" - ], + "columns": ["guest_id"], "isUnique": true }, "users_email_unique": { "name": "users_email_unique", - "columns": [ - "email" - ], + "columns": ["email"], "isUnique": true } }, @@ -1091,4 +1035,4 @@ "internal": { "indexes": {} } -} \ No newline at end of file +} diff --git a/apps/web/drizzle/meta/0063_snapshot.json b/apps/web/drizzle/meta/0063_snapshot.json new file mode 100644 index 00000000..bcbd4410 --- /dev/null +++ b/apps/web/drizzle/meta/0063_snapshot.json @@ -0,0 +1,1038 @@ +{ + "id": "0e60aef4-8e9b-40f0-9dd4-5e9b1a0f9a20", + "prevId": "beacdacf-b94f-427f-95dc-564006cc2c4f", + "version": "6", + "dialect": "sqlite", + "tables": { + "abacus_settings": { + "name": "abacus_settings", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "color_scheme": { + "name": "color_scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'place-value'" + }, + "bead_shape": { + "name": "bead_shape", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'diamond'" + }, + "color_palette": { + "name": "color_palette", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "hide_inactive_beads": { + "name": "hide_inactive_beads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "colored_numerals": { + "name": "colored_numerals", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scale_factor": { + "name": "scale_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "show_numbers": { + "name": "show_numbers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "animated": { + "name": "animated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "interactive": { + "name": "interactive", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "gestures": { + "name": "gestures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sound_enabled": { + "name": "sound_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sound_volume": { + "name": "sound_volume", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0.8 + } + }, + "indexes": {}, + "foreignKeys": { + "abacus_settings_user_id_users_id_fk": { + "name": "abacus_settings_user_id_users_id_fk", + "tableFrom": "abacus_settings", + "columnsFrom": ["user_id"], + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "arcade_rooms": { + "name": "arcade_rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_name": { + "name": "creator_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_activity": { + "name": "last_activity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_minutes": { + "name": "ttl_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "is_locked": { + "name": "is_locked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "game_name": { + "name": "game_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_config": { + "name": "game_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'lobby'" + }, + "current_session_id": { + "name": "current_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_games_played": { + "name": "total_games_played", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "arcade_rooms_code_unique": { + "name": "arcade_rooms_code_unique", + "columns": ["code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "arcade_sessions": { + "name": "arcade_sessions", + "columns": { + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_game": { + "name": "current_game", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_url": { + "name": "game_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_state": { + "name": "game_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_players": { + "name": "active_players", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "arcade_sessions_room_id_arcade_rooms_id_fk": { + "name": "arcade_sessions_room_id_arcade_rooms_id_fk", + "tableFrom": "arcade_sessions", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "arcade_sessions_user_id_users_id_fk": { + "name": "arcade_sessions_user_id_users_id_fk", + "tableFrom": "arcade_sessions", + "columnsFrom": ["user_id"], + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "players": { + "name": "players", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "players_user_id_idx": { + "name": "players_user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "players_user_id_users_id_fk": { + "name": "players_user_id_users_id_fk", + "tableFrom": "players", + "columnsFrom": ["user_id"], + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_members": { + "name": "room_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_creator": { + "name": "is_creator", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "joined_at": { + "name": "joined_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen": { + "name": "last_seen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_online": { + "name": "is_online", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_room_members_user_id_unique": { + "name": "idx_room_members_user_id_unique", + "columns": ["user_id"], + "isUnique": true + } + }, + "foreignKeys": { + "room_members_room_id_arcade_rooms_id_fk": { + "name": "room_members_room_id_arcade_rooms_id_fk", + "tableFrom": "room_members", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_member_history": { + "name": "room_member_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_joined_at": { + "name": "first_joined_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_action": { + "name": "last_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "last_action_at": { + "name": "last_action_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "room_member_history_room_id_arcade_rooms_id_fk": { + "name": "room_member_history_room_id_arcade_rooms_id_fk", + "tableFrom": "room_member_history", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_invitations": { + "name": "room_invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by_name": { + "name": "invited_by_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "invitation_type": { + "name": "invitation_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "message": { + "name": "message", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "responded_at": { + "name": "responded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_room_invitations_user_room": { + "name": "idx_room_invitations_user_room", + "columns": ["user_id", "room_id"], + "isUnique": true + } + }, + "foreignKeys": { + "room_invitations_room_id_arcade_rooms_id_fk": { + "name": "room_invitations_room_id_arcade_rooms_id_fk", + "tableFrom": "room_invitations", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_reports": { + "name": "room_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reporter_id": { + "name": "reporter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reporter_name": { + "name": "reporter_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_user_id": { + "name": "reported_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_user_name": { + "name": "reported_user_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "room_reports_room_id_arcade_rooms_id_fk": { + "name": "room_reports_room_id_arcade_rooms_id_fk", + "tableFrom": "room_reports", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_bans": { + "name": "room_bans", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "banned_by": { + "name": "banned_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "banned_by_name": { + "name": "banned_by_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_room_bans_user_room": { + "name": "idx_room_bans_user_room", + "columns": ["user_id", "room_id"], + "isUnique": true + } + }, + "foreignKeys": { + "room_bans_room_id_arcade_rooms_id_fk": { + "name": "room_bans_room_id_arcade_rooms_id_fk", + "tableFrom": "room_bans", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_stats": { + "name": "user_stats", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "games_played": { + "name": "games_played", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_wins": { + "name": "total_wins", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "favorite_game_type": { + "name": "favorite_game_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "best_time": { + "name": "best_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "highest_accuracy": { + "name": "highest_accuracy", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_users_id_fk": { + "name": "user_stats_user_id_users_id_fk", + "tableFrom": "user_stats", + "columnsFrom": ["user_id"], + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "guest_id": { + "name": "guest_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upgraded_at": { + "name": "upgraded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_guest_id_unique": { + "name": "users_guest_id_unique", + "columns": ["guest_id"], + "isUnique": true + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/web/drizzle/meta/0064_snapshot.json b/apps/web/drizzle/meta/0064_snapshot.json new file mode 100644 index 00000000..5a73894d --- /dev/null +++ b/apps/web/drizzle/meta/0064_snapshot.json @@ -0,0 +1,1038 @@ +{ + "id": "e56745f1-9b50-4da3-8d3d-d7abc09c70f3", + "prevId": "0e60aef4-8e9b-40f0-9dd4-5e9b1a0f9a20", + "version": "6", + "dialect": "sqlite", + "tables": { + "abacus_settings": { + "name": "abacus_settings", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "color_scheme": { + "name": "color_scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'place-value'" + }, + "bead_shape": { + "name": "bead_shape", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'diamond'" + }, + "color_palette": { + "name": "color_palette", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "hide_inactive_beads": { + "name": "hide_inactive_beads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "colored_numerals": { + "name": "colored_numerals", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scale_factor": { + "name": "scale_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "show_numbers": { + "name": "show_numbers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "animated": { + "name": "animated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "interactive": { + "name": "interactive", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "gestures": { + "name": "gestures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sound_enabled": { + "name": "sound_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sound_volume": { + "name": "sound_volume", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0.8 + } + }, + "indexes": {}, + "foreignKeys": { + "abacus_settings_user_id_users_id_fk": { + "name": "abacus_settings_user_id_users_id_fk", + "tableFrom": "abacus_settings", + "columnsFrom": ["user_id"], + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "arcade_rooms": { + "name": "arcade_rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_name": { + "name": "creator_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_activity": { + "name": "last_activity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_minutes": { + "name": "ttl_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "is_locked": { + "name": "is_locked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "game_name": { + "name": "game_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_config": { + "name": "game_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'lobby'" + }, + "current_session_id": { + "name": "current_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_games_played": { + "name": "total_games_played", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "arcade_rooms_code_unique": { + "name": "arcade_rooms_code_unique", + "columns": ["code"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "arcade_sessions": { + "name": "arcade_sessions", + "columns": { + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_game": { + "name": "current_game", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_url": { + "name": "game_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_state": { + "name": "game_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_players": { + "name": "active_players", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "arcade_sessions_room_id_arcade_rooms_id_fk": { + "name": "arcade_sessions_room_id_arcade_rooms_id_fk", + "tableFrom": "arcade_sessions", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "arcade_sessions_user_id_users_id_fk": { + "name": "arcade_sessions_user_id_users_id_fk", + "tableFrom": "arcade_sessions", + "columnsFrom": ["user_id"], + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "players": { + "name": "players", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "players_user_id_idx": { + "name": "players_user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "players_user_id_users_id_fk": { + "name": "players_user_id_users_id_fk", + "tableFrom": "players", + "columnsFrom": ["user_id"], + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_members": { + "name": "room_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_creator": { + "name": "is_creator", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "joined_at": { + "name": "joined_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen": { + "name": "last_seen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_online": { + "name": "is_online", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_room_members_user_id_unique": { + "name": "idx_room_members_user_id_unique", + "columns": ["user_id"], + "isUnique": true + } + }, + "foreignKeys": { + "room_members_room_id_arcade_rooms_id_fk": { + "name": "room_members_room_id_arcade_rooms_id_fk", + "tableFrom": "room_members", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_member_history": { + "name": "room_member_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_joined_at": { + "name": "first_joined_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_action": { + "name": "last_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "last_action_at": { + "name": "last_action_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "room_member_history_room_id_arcade_rooms_id_fk": { + "name": "room_member_history_room_id_arcade_rooms_id_fk", + "tableFrom": "room_member_history", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_invitations": { + "name": "room_invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by_name": { + "name": "invited_by_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "invitation_type": { + "name": "invitation_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "message": { + "name": "message", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "responded_at": { + "name": "responded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_room_invitations_user_room": { + "name": "idx_room_invitations_user_room", + "columns": ["user_id", "room_id"], + "isUnique": true + } + }, + "foreignKeys": { + "room_invitations_room_id_arcade_rooms_id_fk": { + "name": "room_invitations_room_id_arcade_rooms_id_fk", + "tableFrom": "room_invitations", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_reports": { + "name": "room_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reporter_id": { + "name": "reporter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reporter_name": { + "name": "reporter_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_user_id": { + "name": "reported_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_user_name": { + "name": "reported_user_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "room_reports_room_id_arcade_rooms_id_fk": { + "name": "room_reports_room_id_arcade_rooms_id_fk", + "tableFrom": "room_reports", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_bans": { + "name": "room_bans", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "banned_by": { + "name": "banned_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "banned_by_name": { + "name": "banned_by_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_room_bans_user_room": { + "name": "idx_room_bans_user_room", + "columns": ["user_id", "room_id"], + "isUnique": true + } + }, + "foreignKeys": { + "room_bans_room_id_arcade_rooms_id_fk": { + "name": "room_bans_room_id_arcade_rooms_id_fk", + "tableFrom": "room_bans", + "columnsFrom": ["room_id"], + "tableTo": "arcade_rooms", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_stats": { + "name": "user_stats", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "games_played": { + "name": "games_played", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_wins": { + "name": "total_wins", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "favorite_game_type": { + "name": "favorite_game_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "best_time": { + "name": "best_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "highest_accuracy": { + "name": "highest_accuracy", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_users_id_fk": { + "name": "user_stats_user_id_users_id_fk", + "tableFrom": "user_stats", + "columnsFrom": ["user_id"], + "tableTo": "users", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "guest_id": { + "name": "guest_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upgraded_at": { + "name": "upgraded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_guest_id_unique": { + "name": "users_guest_id_unique", + "columns": ["guest_id"], + "isUnique": true + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/web/drizzle/meta/_journal.json b/apps/web/drizzle/meta/_journal.json index c476f420..83aa2e32 100644 --- a/apps/web/drizzle/meta/_journal.json +++ b/apps/web/drizzle/meta/_journal.json @@ -442,6 +442,20 @@ "when": 1768066927761, "tag": "0062_confused_captain_midlands", "breakpoints": true + }, + { + "idx": 63, + "version": "6", + "when": 1768235998923, + "tag": "0063_unique_moondragon", + "breakpoints": true + }, + { + "idx": 64, + "version": "6", + "when": 1768236515704, + "tag": "0064_thin_marvel_apes", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/web/src/app/api/scanner-settings/route.ts b/apps/web/src/app/api/scanner-settings/route.ts new file mode 100644 index 00000000..af9d101b --- /dev/null +++ b/apps/web/src/app/api/scanner-settings/route.ts @@ -0,0 +1,165 @@ +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { db } from '@/db' +import * as schema from '@/db/schema' +import { getViewerId } from '@/lib/viewer' + +/** + * GET /api/scanner-settings + * Fetch scanner settings for the current user + */ +export async function GET() { + try { + const viewerId = await getViewerId() + const user = await getOrCreateUser(viewerId) + + // Find or create scanner settings + let settings = await db.query.scannerSettings.findFirst({ + where: eq(schema.scannerSettings.userId, user.id), + }) + + // If no settings exist, create with defaults + if (!settings) { + const [newSettings] = await db + .insert(schema.scannerSettings) + .values({ userId: user.id }) + .returning() + settings = newSettings + } + + // Transform database format to QuadDetectorConfig format + const config = { + preprocessing: settings.preprocessing, + enableHistogramEqualization: settings.enableHistogramEqualization, + enableAdaptiveThreshold: settings.enableAdaptiveThreshold, + enableMorphGradient: settings.enableMorphGradient, + cannyThresholds: [settings.cannyLow, settings.cannyHigh] as [number, number], + adaptiveBlockSize: settings.adaptiveBlockSize, + adaptiveC: settings.adaptiveC, + enableHoughLines: settings.enableHoughLines, + } + + return NextResponse.json({ settings: config }) + } catch (error) { + console.error('Failed to fetch scanner settings:', error) + return NextResponse.json({ error: 'Failed to fetch scanner settings' }, { status: 500 }) + } +} + +/** + * PATCH /api/scanner-settings + * Update scanner settings for the current user + */ +export async function PATCH(req: NextRequest) { + try { + const viewerId = await getViewerId() + + // Handle empty or invalid JSON body gracefully + let body: Record + try { + body = await req.json() + } catch { + return NextResponse.json({ error: 'Invalid or empty request body' }, { status: 400 }) + } + + // Security: Strip userId from request body - it must come from session only + const { userId: _, ...updates } = body + + // Transform QuadDetectorConfig format to database format + const dbUpdates: Record = {} + + if (updates.preprocessing !== undefined) { + dbUpdates.preprocessing = updates.preprocessing + } + if (updates.enableHistogramEqualization !== undefined) { + dbUpdates.enableHistogramEqualization = updates.enableHistogramEqualization + } + if (updates.enableAdaptiveThreshold !== undefined) { + dbUpdates.enableAdaptiveThreshold = updates.enableAdaptiveThreshold + } + if (updates.enableMorphGradient !== undefined) { + dbUpdates.enableMorphGradient = updates.enableMorphGradient + } + if (updates.cannyThresholds !== undefined) { + const thresholds = updates.cannyThresholds as [number, number] + dbUpdates.cannyLow = thresholds[0] + dbUpdates.cannyHigh = thresholds[1] + } + if (updates.adaptiveBlockSize !== undefined) { + dbUpdates.adaptiveBlockSize = updates.adaptiveBlockSize + } + if (updates.adaptiveC !== undefined) { + dbUpdates.adaptiveC = updates.adaptiveC + } + if (updates.enableHoughLines !== undefined) { + dbUpdates.enableHoughLines = updates.enableHoughLines + } + + const user = await getOrCreateUser(viewerId) + + // Ensure settings exist + const existingSettings = await db.query.scannerSettings.findFirst({ + where: eq(schema.scannerSettings.userId, user.id), + }) + + let resultSettings: schema.ScannerSettings + + if (!existingSettings) { + // Create new settings with updates + const [newSettings] = await db + .insert(schema.scannerSettings) + .values({ userId: user.id, ...dbUpdates }) + .returning() + resultSettings = newSettings + } else { + // Update existing settings + const [updatedSettings] = await db + .update(schema.scannerSettings) + .set(dbUpdates) + .where(eq(schema.scannerSettings.userId, user.id)) + .returning() + resultSettings = updatedSettings + } + + // Transform back to QuadDetectorConfig format + const config = { + preprocessing: resultSettings.preprocessing, + enableHistogramEqualization: resultSettings.enableHistogramEqualization, + enableAdaptiveThreshold: resultSettings.enableAdaptiveThreshold, + enableMorphGradient: resultSettings.enableMorphGradient, + cannyThresholds: [resultSettings.cannyLow, resultSettings.cannyHigh] as [number, number], + adaptiveBlockSize: resultSettings.adaptiveBlockSize, + adaptiveC: resultSettings.adaptiveC, + enableHoughLines: resultSettings.enableHoughLines, + } + + return NextResponse.json({ settings: config }) + } catch (error) { + console.error('Failed to update scanner settings:', error) + return NextResponse.json({ error: 'Failed to update scanner settings' }, { status: 500 }) + } +} + +/** + * Get or create a user record for the given viewer ID (guest or user) + */ +async function getOrCreateUser(viewerId: string) { + // Try to find existing user by guest ID + let user = await db.query.users.findFirst({ + where: eq(schema.users.guestId, viewerId), + }) + + // If no user exists, create one + if (!user) { + const [newUser] = await db + .insert(schema.users) + .values({ + guestId: viewerId, + }) + .returning() + + user = newUser + } + + return user +} diff --git a/apps/web/src/app/practice/[studentId]/summary/FullscreenCamera.tsx b/apps/web/src/app/practice/[studentId]/summary/FullscreenCamera.tsx index 0efa06bb..2e363208 100644 --- a/apps/web/src/app/practice/[studentId]/summary/FullscreenCamera.tsx +++ b/apps/web/src/app/practice/[studentId]/summary/FullscreenCamera.tsx @@ -3,6 +3,9 @@ import dynamic from 'next/dynamic' import { useCallback, useEffect, useRef, useState } from 'react' import { useDocumentDetection } from '@/components/practice/useDocumentDetection' +import { ScannerControlsDrawer } from '@/components/practice/ScannerControlsDrawer' +import { useScannerSettings, useUpdateScannerSettings } from '@/hooks/useScannerSettings' +import type { QuadDetectorConfig } from '@/lib/vision/quadDetector' import { css } from '../../../../../styled-system/css' // Dynamic import for DocumentAdjuster (pulls in OpenCV) @@ -11,6 +14,108 @@ const DocumentAdjuster = dynamic( { ssr: false } ) +// Lighting presets for different conditions +const LIGHTING_PRESETS = { + normal: { + label: 'Normal', + icon: '☀️', + config: { + preprocessing: 'multi' as const, + enableHistogramEqualization: true, + enableAdaptiveThreshold: true, + enableMorphGradient: true, + cannyThresholds: [50, 150] as [number, number], + adaptiveBlockSize: 11, + adaptiveC: 2, + }, + }, + lowLight: { + label: 'Low Light', + icon: '🌙', + config: { + preprocessing: 'multi' as const, + enableHistogramEqualization: true, + enableAdaptiveThreshold: true, + enableMorphGradient: true, + cannyThresholds: [30, 100] as [number, number], + adaptiveBlockSize: 15, + adaptiveC: 5, + }, + }, + bright: { + label: 'Bright', + icon: '🔆', + config: { + preprocessing: 'enhanced' as const, + enableHistogramEqualization: true, + enableAdaptiveThreshold: false, + enableMorphGradient: false, + cannyThresholds: [80, 200] as [number, number], + adaptiveBlockSize: 11, + adaptiveC: 2, + }, + }, +} + +// Analyze video frame to determine best preset +function analyzeFrameLighting(video: HTMLVideoElement): { + preset: 'normal' | 'lowLight' | 'bright' + brightness: number + contrast: number +} { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + if (!ctx) return { preset: 'normal', brightness: 128, contrast: 50 } + + // Sample at lower resolution for speed + const sampleWidth = 160 + const sampleHeight = 120 + canvas.width = sampleWidth + canvas.height = sampleHeight + + ctx.drawImage(video, 0, 0, sampleWidth, sampleHeight) + const imageData = ctx.getImageData(0, 0, sampleWidth, sampleHeight) + const data = imageData.data + + // Calculate luminance for each pixel (simple average) + let totalLuminance = 0 + const luminances: number[] = [] + const pixelCount = data.length / 4 + + for (let i = 0; i < data.length; i += 4) { + // Weighted luminance: 0.299*R + 0.587*G + 0.114*B + const luminance = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2] + luminances.push(luminance) + totalLuminance += luminance + } + + const avgBrightness = totalLuminance / pixelCount + + // Calculate contrast (standard deviation) + let varianceSum = 0 + for (const lum of luminances) { + varianceSum += (lum - avgBrightness) ** 2 + } + const contrast = Math.sqrt(varianceSum / pixelCount) + + // Determine preset based on metrics + let preset: 'normal' | 'lowLight' | 'bright' = 'normal' + + if (avgBrightness < 70) { + // Dark scene + preset = 'lowLight' + } else if (avgBrightness > 180 && contrast < 40) { + // Bright and washed out + preset = 'bright' + } else if (avgBrightness > 160) { + // Just bright + preset = 'bright' + } + // Otherwise normal + + return { preset, brightness: avgBrightness, contrast } +} + interface FullscreenCameraProps { /** Called with cropped file, original file, corners, and rotation for later re-editing */ onCapture: ( @@ -28,7 +133,6 @@ export function FullscreenCamera({ onCapture, onClose }: FullscreenCameraProps) const overlayCanvasRef = useRef(null) const streamRef = useRef(null) const animationFrameRef = useRef(null) - const lastDetectionRef = useRef(0) const autoCaptureTriggeredRef = useRef(false) const [isReady, setIsReady] = useState(false) @@ -36,12 +140,33 @@ export function FullscreenCamera({ onCapture, onClose }: FullscreenCameraProps) const [isCapturing, setIsCapturing] = useState(false) const [documentDetected, setDocumentDetected] = useState(false) + // Camera controls state + const [torchAvailable, setTorchAvailable] = useState(false) + const [torchOn, setTorchOn] = useState(false) + const [hasMultipleCameras, setHasMultipleCameras] = useState(false) + const [facingMode, setFacingMode] = useState<'environment' | 'user'>('environment') + + // Preset selector state + const [presetMode, setPresetMode] = useState<'auto' | 'normal' | 'lowLight' | 'bright'>('auto') + const [detectedPreset, setDetectedPreset] = useState<'normal' | 'lowLight' | 'bright'>('normal') + const [presetPopoverOpen, setPresetPopoverOpen] = useState(false) + const [fingerOcclusionMode, setFingerOcclusionMode] = useState(false) + // Adjustment mode state const [adjustmentMode, setAdjustmentMode] = useState<{ sourceCanvas: HTMLCanvasElement corners: Array<{ x: number; y: number }> } | null>(null) + // Advanced controls drawer state + const [isDrawerOpen, setIsDrawerOpen] = useState(false) + const drawerSwipeRef = useRef<{ startX: number; startY: number } | null>(null) + const settingsInitializedRef = useRef(false) + + // Load persisted scanner settings from database + const { data: savedSettings } = useScannerSettings() + const updateSettingsMutation = useUpdateScannerSettings() + // Document detection hook (lazy loads OpenCV.js) const { isLoading: isScannerLoading, @@ -55,8 +180,101 @@ export function FullscreenCamera({ onCapture, onClose }: FullscreenCameraProps) captureSourceFrame, highlightDocument, detectQuadsInImage: detectQuadsInCamera, + resetTracking, + updateDetectorConfig, + detectorConfig, } = useDocumentDetection() + // Initialize detector config with saved settings when they load + useEffect(() => { + if (savedSettings && !settingsInitializedRef.current) { + settingsInitializedRef.current = true + updateDetectorConfig(savedSettings) + } + }, [savedSettings, updateDetectorConfig]) + + // Handle config changes - update both local state and persist to database + const handleConfigChange = useCallback( + (newConfig: Partial) => { + // Update local detector immediately for instant feedback + updateDetectorConfig(newConfig) + // Persist to database (uses optimistic update) + updateSettingsMutation.mutate(newConfig) + }, + [updateDetectorConfig, updateSettingsMutation] + ) + + // Apply preset config when preset mode or finger occlusion changes + const applyPresetConfig = useCallback( + (presetKey: 'normal' | 'lowLight' | 'bright') => { + const preset = LIGHTING_PRESETS[presetKey] + const config = { ...preset.config } + + // Add finger occlusion enhancements if enabled + if (fingerOcclusionMode) { + config.enableHoughLines = true + config.enableMorphGradient = true + // Slightly lower thresholds to catch partial edges + config.cannyThresholds = [ + Math.max(20, config.cannyThresholds[0] - 15), + Math.max(80, config.cannyThresholds[1] - 30), + ] as [number, number] + } + + updateDetectorConfig(config) + }, + [fingerOcclusionMode, updateDetectorConfig] + ) + + // Analyze frame periodically when in auto mode + useEffect(() => { + if (presetMode !== 'auto' || !isReady || !videoRef.current || adjustmentMode) return + + const analyzeInterval = setInterval(() => { + if (videoRef.current) { + const analysis = analyzeFrameLighting(videoRef.current) + if (analysis.preset !== detectedPreset) { + setDetectedPreset(analysis.preset) + applyPresetConfig(analysis.preset) + } + } + }, 2000) // Analyze every 2 seconds + + // Initial analysis + const analysis = analyzeFrameLighting(videoRef.current) + setDetectedPreset(analysis.preset) + applyPresetConfig(analysis.preset) + + return () => clearInterval(analyzeInterval) + }, [presetMode, isReady, adjustmentMode, detectedPreset, applyPresetConfig]) + + // Apply preset when manually selected + useEffect(() => { + if (presetMode !== 'auto') { + applyPresetConfig(presetMode) + } + }, [presetMode, applyPresetConfig]) + + // Reapply current preset when finger occlusion mode changes + useEffect(() => { + const currentPreset = presetMode === 'auto' ? detectedPreset : presetMode + applyPresetConfig(currentPreset) + }, [fingerOcclusionMode, presetMode, detectedPreset, applyPresetConfig]) + + // Check for multiple cameras on mount + useEffect(() => { + const checkCameras = async () => { + try { + const devices = await navigator.mediaDevices.enumerateDevices() + const videoDevices = devices.filter((d) => d.kind === 'videoinput') + setHasMultipleCameras(videoDevices.length > 1) + } catch { + // Ignore errors - just won't show flip button + } + } + checkCameras() + }, []) + useEffect(() => { let cancelled = false @@ -68,7 +286,7 @@ export function FullscreenCamera({ onCapture, onClose }: FullscreenCameraProps) const constraints: MediaStreamConstraints = { video: { - facingMode: { ideal: 'environment' }, + facingMode: { ideal: facingMode }, width: { ideal: 1920 }, height: { ideal: 1080 }, }, @@ -84,6 +302,16 @@ export function FullscreenCamera({ onCapture, onClose }: FullscreenCameraProps) streamRef.current = stream + // Check for torch capability + const videoTrack = stream.getVideoTracks()[0] + if (videoTrack) { + const capabilities = videoTrack.getCapabilities?.() + // @ts-expect-error - torch is not in the standard types but exists on mobile + const hasTorch = capabilities?.torch === true + setTorchAvailable(hasTorch) + setTorchOn(false) // Reset torch state when camera changes + } + if (videoRef.current) { videoRef.current.srcObject = stream await videoRef.current.play() @@ -110,11 +338,12 @@ export function FullscreenCamera({ onCapture, onClose }: FullscreenCameraProps) streamRef.current = null } } - }, [ensureOpenCVLoaded]) + }, [ensureOpenCVLoaded, facingMode]) - // Detection loop - runs when camera and scanner are ready + // Detection loop - runs when camera and scanner are ready, and NOT in adjustment mode useEffect(() => { - if (!isReady || !isScannerReady) return + // Don't run detection loop while in adjustment mode + if (!isReady || !isScannerReady || adjustmentMode) return const video = videoRef.current const overlay = overlayCanvasRef.current @@ -130,14 +359,11 @@ export function FullscreenCamera({ onCapture, onClose }: FullscreenCameraProps) syncCanvasSize() const detectLoop = () => { - const now = Date.now() - // Throttle detection to every 150ms for performance - if (now - lastDetectionRef.current > 150) { - if (video && overlay) { - const detected = highlightDocument(video, overlay) - setDocumentDetected(detected) - } - lastDetectionRef.current = now + // Run detection every frame for smooth tracking + // Detection typically takes ~8-10ms, well within 60fps budget + if (video && overlay) { + const detected = highlightDocument(video, overlay) + setDocumentDetected(detected) } animationFrameRef.current = requestAnimationFrame(detectLoop) } @@ -155,7 +381,7 @@ export function FullscreenCamera({ onCapture, onClose }: FullscreenCameraProps) } window.removeEventListener('resize', syncCanvasSize) } - }, [isReady, isScannerReady, highlightDocument]) + }, [isReady, isScannerReady, highlightDocument, adjustmentMode]) // Enter adjustment mode with captured frame and detected corners // Always shows the adjustment UI - uses fallback corners if no quad detected @@ -248,49 +474,84 @@ export function FullscreenCamera({ onCapture, onClose }: FullscreenCameraProps) ) // Handle adjustment cancel - return to camera + // The detection loop will automatically restart via useEffect when adjustmentMode becomes null const handleAdjustmentCancel = useCallback(() => { setAdjustmentMode(null) autoCaptureTriggeredRef.current = false // Allow auto-capture again - // Restart detection loop - if (videoRef.current && overlayCanvasRef.current && isScannerReady) { - const detectLoop = () => { - const now = Date.now() - if (now - lastDetectionRef.current > 150) { - if (videoRef.current && overlayCanvasRef.current) { - const detected = highlightDocument(videoRef.current, overlayCanvasRef.current) - setDocumentDetected(detected) - } - lastDetectionRef.current = now - } - animationFrameRef.current = requestAnimationFrame(detectLoop) - } - animationFrameRef.current = requestAnimationFrame(detectLoop) - } - }, [isScannerReady, highlightDocument]) + resetTracking() // Clear old quad detection state + }, [resetTracking]) - // Show adjustment UI if in adjustment mode - if (adjustmentMode && opencvRef) { - return ( - - ) - } + // Show adjustment UI if in adjustment mode (overlay on top of camera) + // Keep camera mounted but hidden to preserve video stream + const showAdjuster = !!(adjustmentMode && opencvRef) + + // Toggle torch/flashlight + const toggleTorch = useCallback(async () => { + if (!streamRef.current || !torchAvailable) return + + const videoTrack = streamRef.current.getVideoTracks()[0] + if (!videoTrack) return + + try { + const newTorchState = !torchOn + await videoTrack.applyConstraints({ + // @ts-expect-error - advanced torch constraint exists on mobile + advanced: [{ torch: newTorchState }], + }) + setTorchOn(newTorchState) + } catch (err) { + console.error('Failed to toggle torch:', err) + } + }, [torchAvailable, torchOn]) + + // Flip between front and back camera + const flipCamera = useCallback(() => { + // Reset tracking and torch state before switching + resetTracking() + setTorchOn(false) + setIsReady(false) + setFacingMode((prev) => (prev === 'environment' ? 'user' : 'environment')) + }, [resetTracking]) + + // Swipe handlers for drawer + const handleTouchStart = useCallback((e: React.TouchEvent) => { + const touch = e.touches[0] + // Only track swipes starting from left edge (first 30px) + if (touch.clientX < 30) { + drawerSwipeRef.current = { startX: touch.clientX, startY: touch.clientY } + } + }, []) + + const handleTouchMove = useCallback((e: React.TouchEvent) => { + if (!drawerSwipeRef.current) return + const touch = e.touches[0] + const deltaX = touch.clientX - drawerSwipeRef.current.startX + const deltaY = Math.abs(touch.clientY - drawerSwipeRef.current.startY) + + // If swiping right and more horizontal than vertical, open drawer + if (deltaX > 50 && deltaX > deltaY * 2) { + setIsDrawerOpen(true) + drawerSwipeRef.current = null + } + }, []) + + const handleTouchEnd = useCallback(() => { + drawerSwipeRef.current = null + }, []) return (
+ {/* Always render video to keep stream alive */}