mysql_genius-core 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/lib/mysql_genius/core/ai/config.rb +5 -2
  4. data/lib/mysql_genius/core/ai/describe_query.rb +41 -0
  5. data/lib/mysql_genius/core/ai/index_advisor.rb +43 -0
  6. data/lib/mysql_genius/core/ai/migration_risk.rb +51 -0
  7. data/lib/mysql_genius/core/ai/rewrite_query.rb +51 -0
  8. data/lib/mysql_genius/core/ai/schema_context_builder.rb +82 -0
  9. data/lib/mysql_genius/core/ai/schema_review.rb +46 -0
  10. data/lib/mysql_genius/core/analysis/columns.rb +64 -0
  11. data/lib/mysql_genius/core/version.rb +1 -1
  12. data/lib/mysql_genius/core/views/mysql_genius/queries/_shared_results.html.erb +56 -0
  13. data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_ai_tools.html.erb +43 -0
  14. data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_dashboard.html.erb +95 -0
  15. data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_duplicate_indexes.html.erb +35 -0
  16. data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_query_explorer.html.erb +110 -0
  17. data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_query_stats.html.erb +26 -0
  18. data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_server.html.erb +54 -0
  19. data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_slow_queries.html.erb +17 -0
  20. data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_table_sizes.html.erb +33 -0
  21. data/lib/mysql_genius/core/views/mysql_genius/queries/_tab_unused_indexes.html.erb +36 -0
  22. data/lib/mysql_genius/core/views/mysql_genius/queries/dashboard.html.erb +1565 -0
  23. data/lib/mysql_genius/core.rb +18 -0
  24. data/mysql_genius-core.gemspec +1 -1
  25. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba1c5800131408bf90c87a052719196ebca6c84c9a9eae175e205a027d24e7a4
4
- data.tar.gz: e5ae1079f22dea06691754b08b685550fb23b68c1b3dbec3f455ad9e38dc8436
3
+ metadata.gz: fb359baa1d884ae70644cfde4e97a047c2d18066f36fe9e100fbc497b5fd06c2
4
+ data.tar.gz: 5084afa18011bb5b0dad34c74a53bad6f84d3c13fc27b4c0a5554efab9be0295
5
5
  SHA512:
6
- metadata.gz: 6142e5bd5cf98d4c1bda216240f48889e840af67d317fe9cb5b3c12afb48b252d37409fe652ada956c11867fe0d3ba98681ac41b1eda273be339f54c4b5404af
7
- data.tar.gz: addd52a4e81dc87af3f5eec1f016208a1cd22b8a018fd471722d460af6d911a05241d771e2ec4d6765acde3cb52b47ad4b8f7346787231722377862d0a50c0e1
6
+ metadata.gz: 06f54a635d51eaedc69367261ef87455670e082105f5253f606197aca27427df0f156b0c6977745eabb5bfb2567d48e61e8302c969f098de4515542264b949d0
7
+ data.tar.gz: a0e09ec4b55f557038f26899f6398a4e68cdb0c3443e72662c63e6812daae3e8d8504a8cded994615cbd612a16313f4809ccdfca65a0a0aa35760cbc586ed182
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Fixed
6
+ - **ERB templates missing from gem package.** The `spec.files` glob only matched `*.rb`, excluding the shared ERB templates at `lib/mysql_genius/core/views/`. Fixed by changing to `*.{rb,erb}`.
7
+
8
+ ## 0.5.0
9
+
10
+ ### Added
11
+ - `MysqlGenius::Core::Analysis::Columns` — service class for `GET /columns` logic with a tagged-result struct (`:ok`, `:blocked`, `:not_found`). Takes a `Core::Connection`, uses `Core::SqlValidator.masked_column?` for the masked-column rule.
12
+ - `MysqlGenius::Core::Ai::{DescribeQuery, SchemaReview, RewriteQuery, IndexAdvisor, MigrationRisk}` — 5 AI prompt builder classes extracted from the `mysql_genius` Rails adapter's `AiFeatures` concern. Each takes `(client, config)` or `(client, config, connection)` and exposes a single `#call` method.
13
+ - `MysqlGenius::Core::Ai::SchemaContextBuilder` — shared helper for building "Table: X (~N rows), Columns: …, Indexes: …" schema descriptions. Supports `detail: :basic` and `detail: :with_cardinality`.
14
+ - `MysqlGenius::Core::Ai::Config#domain_context` — new optional keyword field (empty string default) interpolated into every extracted prompt builder's system prompt.
15
+ - `MysqlGenius::Core.views_path` — public module method returning the absolute path to the shared ERB template directory. Adapters register this path with their own view loader.
16
+ - **ERB templates extracted from the Rails adapter.** `MysqlGenius::Core.views_path` now points at `lib/mysql_genius/core/views/` which contains `mysql_genius/queries/dashboard.html.erb` and the 10 tab/partial files. Any adapter (Rails, Sinatra, or future desktop) can load these templates by registering this path with its own view loader. Templates depend on a minimal 2-method contract: `path_for(name)` and `render_partial(name)`.
17
+
3
18
  ## 0.4.1
4
19
 
5
20
  No functional changes in `mysql_genius-core`. Version bumped to maintain lockstep with `mysql_genius 0.4.1`, which hotfixes a regression in the Rails adapter's `GET /columns` endpoint. See the root `CHANGELOG.md` for details.
@@ -16,6 +16,8 @@ module MysqlGenius
16
16
  # auth_style - :bearer or :api_key
17
17
  # system_context - optional domain context string that services
18
18
  # append to their system prompts
19
+ # domain_context - optional host-app context string interpolated into
20
+ # AI system prompts (e.g. "Rails app, no FKs")
19
21
  Config = Struct.new(
20
22
  :client,
21
23
  :endpoint,
@@ -23,10 +25,11 @@ module MysqlGenius
23
25
  :model,
24
26
  :auth_style,
25
27
  :system_context,
28
+ :domain_context,
26
29
  keyword_init: true,
27
30
  ) do
28
- def initialize(*)
29
- super
31
+ def initialize(**kwargs)
32
+ super(domain_context: "", **kwargs)
30
33
  freeze
31
34
  end
32
35
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Builds and sends a "describe this query" prompt to Core::Ai::Client.
7
+ # Pure function of SQL + config.domain_context — no connection lookup.
8
+ #
9
+ # Extracted from app/controllers/concerns/mysql_genius/ai_features.rb
10
+ # in Phase 2a.
11
+ class DescribeQuery
12
+ def initialize(client, config)
13
+ @client = client
14
+ @config = config
15
+ end
16
+
17
+ def call(sql)
18
+ messages = [
19
+ { role: "system", content: system_prompt },
20
+ { role: "user", content: sql },
21
+ ]
22
+ @client.chat(messages: messages)
23
+ end
24
+
25
+ private
26
+
27
+ def system_prompt
28
+ <<~PROMPT
29
+ You are a MySQL query explainer. Given a SQL query, explain in plain English:
30
+ 1. What the query does (tables involved, joins, filters, aggregations)
31
+ 2. How data flows through the query
32
+ 3. Any subtle behaviors (implicit type casts, NULL handling in NOT IN, DISTINCT effects, etc.)
33
+ 4. Potential performance concerns visible from the SQL structure alone
34
+ #{@config.domain_context}
35
+ Respond with JSON: {"explanation": "your plain-English explanation using markdown formatting"}
36
+ PROMPT
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ class IndexAdvisor
7
+ def initialize(client, config, connection)
8
+ @client = client
9
+ @config = config
10
+ @connection = connection
11
+ end
12
+
13
+ def call(sql, explain_rows)
14
+ tables = SqlValidator.extract_table_references(sql, @connection)
15
+ schema = SchemaContextBuilder.new(@connection).call(tables, detail: :with_cardinality)
16
+ explain_text = explain_rows.map { |row| row.join(" | ") }.join("\n")
17
+
18
+ messages = [
19
+ { role: "system", content: system_prompt },
20
+ { role: "user", content: "Query:\n#{sql}\n\nEXPLAIN:\n#{explain_text}\n\nSchema:\n#{schema}" },
21
+ ]
22
+ @client.chat(messages: messages)
23
+ end
24
+
25
+ private
26
+
27
+ def system_prompt
28
+ <<~PROMPT
29
+ You are a MySQL index advisor. Given a query, its EXPLAIN output, and current index/cardinality information, suggest optimal indexes. Consider:
30
+ - Composite index column ordering (most selective first, or matching query order)
31
+ - Covering indexes to avoid table lookups
32
+ - Partial indexes for long string columns
33
+ - Write-side costs (if this is a high-write table, note the INSERT/UPDATE overhead)
34
+ - Whether existing indexes could be extended rather than creating new ones
35
+ #{@config.domain_context}
36
+
37
+ Respond with JSON: {"indexes": "markdown-formatted recommendations with exact CREATE INDEX statements, rationale for column ordering, and estimated impact. Include any indexes that should be DROPPED as part of the change."}
38
+ PROMPT
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ class MigrationRisk
7
+ def initialize(client, config, connection)
8
+ @client = client
9
+ @config = config
10
+ @connection = connection
11
+ end
12
+
13
+ def call(migration_sql)
14
+ tables = extract_table_names(migration_sql)
15
+ schema = SchemaContextBuilder.new(@connection).call(tables, detail: :basic)
16
+ schema_text = schema.to_s.empty? ? "Could not determine" : schema
17
+
18
+ messages = [
19
+ { role: "system", content: system_prompt },
20
+ { role: "user", content: "Migration:\n#{migration_sql}\n\nAffected Tables:\n#{schema_text}" },
21
+ ]
22
+ @client.chat(messages: messages)
23
+ end
24
+
25
+ private
26
+
27
+ def extract_table_names(migration_sql)
28
+ # Match Rails migration helpers and raw SQL ALTER TABLE statements.
29
+ rails_matches = migration_sql.scan(/(?:create_table|add_column|remove_column|add_index|remove_index|rename_column|change_column|alter\s+table)\s+[:"]?(\w+)/i).flatten
30
+ sql_matches = migration_sql.scan(/ALTER\s+TABLE\s+`?(\w+)`?/i).flatten
31
+ (rails_matches + sql_matches).uniq
32
+ end
33
+
34
+ def system_prompt
35
+ <<~PROMPT
36
+ You are a MySQL migration risk assessor. Given a Rails migration or DDL, evaluate:
37
+ 1. Will this lock the table? For how long given the row count?
38
+ 2. Is this safe to run during traffic, or does it need a maintenance window?
39
+ 3. Should pt-online-schema-change or gh-ost be used instead?
40
+ 4. Will it break or degrade any of the active queries against this table?
41
+ 5. Are there any data loss risks?
42
+ 6. What is the recommended deployment strategy?
43
+ #{@config.domain_context}
44
+
45
+ Respond with JSON: {"risk_level": "low|medium|high|critical", "assessment": "markdown-formatted risk assessment with specific recommendations and estimated lock duration"}
46
+ PROMPT
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Suggests a rewritten version of a SQL query based on the schema
7
+ # context of the tables it references.
8
+ class RewriteQuery
9
+ def initialize(client, config, connection)
10
+ @client = client
11
+ @config = config
12
+ @connection = connection
13
+ end
14
+
15
+ def call(sql)
16
+ tables = SqlValidator.extract_table_references(sql, @connection)
17
+ schema = SchemaContextBuilder.new(@connection).call(tables, detail: :basic)
18
+
19
+ messages = [
20
+ { role: "system", content: system_prompt(schema) },
21
+ { role: "user", content: sql },
22
+ ]
23
+ @client.chat(messages: messages)
24
+ end
25
+
26
+ private
27
+
28
+ def system_prompt(schema)
29
+ <<~PROMPT
30
+ You are a MySQL query rewrite expert. Analyze the SQL for anti-patterns and suggest a rewritten version. Look for:
31
+ - SELECT * when specific columns would suffice
32
+ - Correlated subqueries that could be JOINs
33
+ - OR conditions preventing index use (suggest UNION ALL)
34
+ - LIKE '%prefix' patterns (leading wildcard)
35
+ - Implicit type conversions in WHERE clauses
36
+ - NOT IN with NULLable columns (suggest NOT EXISTS)
37
+ - ORDER BY on non-indexed columns with LIMIT
38
+ - Unnecessary DISTINCT
39
+ - Functions on indexed columns in WHERE (e.g., DATE(created_at) instead of range)
40
+
41
+ Available schema:
42
+ #{schema}
43
+ #{@config.domain_context}
44
+
45
+ Respond with JSON: {"original": "the original SQL", "rewritten": "the improved SQL", "changes": "markdown list of each change and why it helps"}
46
+ PROMPT
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Builds formatted schema-description strings for AI prompt context.
7
+ # Used by SchemaReview, RewriteQuery, IndexAdvisor, and MigrationRisk.
8
+ #
9
+ # Consolidates the ~10 lines of schema description logic that were
10
+ # duplicated across 4 AI features in the Rails adapter's
11
+ # app/controllers/concerns/mysql_genius/ai_features.rb.
12
+ class SchemaContextBuilder
13
+ def initialize(connection)
14
+ @connection = connection
15
+ end
16
+
17
+ # Returns a formatted multi-line string describing the given tables.
18
+ #
19
+ # detail:
20
+ # :basic — name, row count, primary key, columns, indexes
21
+ # :with_cardinality — adds information_schema.STATISTICS cardinality per index
22
+ def call(tables, detail: :basic)
23
+ Array(tables).filter_map { |t| describe_table(t, detail: detail) }.join("\n\n")
24
+ end
25
+
26
+ private
27
+
28
+ def describe_table(table, detail:)
29
+ return unless @connection.tables.include?(table)
30
+
31
+ cols = @connection.columns_for(table).map do |c|
32
+ parts = ["#{c.name} #{c.sql_type}"]
33
+ parts << "NOT NULL" unless c.null
34
+ parts << "DEFAULT #{c.default}" if c.default
35
+ parts.join(" ")
36
+ end
37
+
38
+ pk = @connection.primary_key(table)
39
+ indexes = @connection.indexes_for(table).map do |idx|
40
+ "#{"UNIQUE " if idx.unique}INDEX #{idx.name} (#{idx.columns.join(", ")})"
41
+ end
42
+
43
+ row_count = fetch_row_count(table)
44
+
45
+ parts = [
46
+ "Table: #{table} (~#{row_count || "unknown"} rows)",
47
+ "Primary Key: #{pk || "NONE"}",
48
+ "Columns: #{cols.join(", ")}",
49
+ "Indexes: #{indexes.any? ? indexes.join(", ") : "NONE"}",
50
+ ]
51
+
52
+ parts << index_cardinality(table) if detail == :with_cardinality
53
+
54
+ parts.join("\n")
55
+ end
56
+
57
+ def fetch_row_count(table)
58
+ sql = "SELECT TABLE_ROWS FROM information_schema.tables " \
59
+ "WHERE table_schema = #{@connection.quote(@connection.current_database)} " \
60
+ "AND table_name = #{@connection.quote(table)}"
61
+ result = @connection.exec_query(sql)
62
+ result.rows.first&.first
63
+ rescue StandardError
64
+ nil
65
+ end
66
+
67
+ def index_cardinality(table)
68
+ sql = "SELECT INDEX_NAME, COLUMN_NAME, CARDINALITY, SEQ_IN_INDEX " \
69
+ "FROM information_schema.STATISTICS " \
70
+ "WHERE TABLE_SCHEMA = #{@connection.quote(@connection.current_database)} " \
71
+ "AND TABLE_NAME = #{@connection.quote(table)} " \
72
+ "ORDER BY INDEX_NAME, SEQ_IN_INDEX"
73
+ result = @connection.exec_query(sql)
74
+ stats = result.rows.map { |r| "#{r[0]}.#{r[1]}: cardinality=#{r[2]}" }
75
+ "Cardinality: #{stats.join(", ")}"
76
+ rescue StandardError
77
+ "Cardinality: (unavailable)"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Ai
6
+ # Reviews a schema for anti-patterns. Takes a specific table name
7
+ # (or nil to review the top 20 queryable tables).
8
+ class SchemaReview
9
+ def initialize(client, config, connection)
10
+ @client = client
11
+ @config = config
12
+ @connection = connection
13
+ end
14
+
15
+ def call(table)
16
+ tables_to_review = table.nil? || table.to_s.empty? ? @connection.tables.first(20) : [table]
17
+ schema_desc = SchemaContextBuilder.new(@connection).call(tables_to_review, detail: :basic)
18
+
19
+ messages = [
20
+ { role: "system", content: system_prompt },
21
+ { role: "user", content: schema_desc },
22
+ ]
23
+ @client.chat(messages: messages)
24
+ end
25
+
26
+ private
27
+
28
+ def system_prompt
29
+ <<~PROMPT
30
+ You are a MySQL schema reviewer. Analyze the following schema and identify anti-patterns and improvement opportunities. Look for:
31
+ - Inappropriate column types (VARCHAR(255) for short values, TEXT where VARCHAR suffices, INT for booleans)
32
+ - Missing indexes on foreign key columns or frequently filtered columns
33
+ - Missing NOT NULL constraints where NULLs are unlikely
34
+ - ENUM columns that should be lookup tables
35
+ - Missing created_at/updated_at timestamps
36
+ - Tables without a PRIMARY KEY
37
+ - Overly wide indexes or redundant indexes
38
+ - Column naming inconsistencies
39
+ #{@config.domain_context}
40
+ Respond with JSON: {"findings": "markdown-formatted findings organized by severity (Critical, Warning, Suggestion). Include specific ALTER TABLE statements where applicable."}
41
+ PROMPT
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlGenius
4
+ module Core
5
+ module Analysis
6
+ # Service class for the Rails engine's GET /columns action and its
7
+ # Phase 2b sidecar equivalent. Takes a Core::Connection and the
8
+ # relevant configuration; returns a tagged Result struct that
9
+ # adapters map to HTTP responses.
10
+ #
11
+ # Each status maps 1:1 to an HTTP status code:
12
+ # :ok → 200 with columns: array
13
+ # :blocked → 403 with error_message:
14
+ # :not_found → 404 with error_message:
15
+ #
16
+ # The adapter reads result.status and dispatches accordingly.
17
+ class Columns
18
+ Result = Struct.new(:status, :columns, :error_message, keyword_init: true)
19
+
20
+ def initialize(connection, blocked_tables:, masked_column_patterns:, default_columns:)
21
+ @connection = connection
22
+ @blocked_tables = blocked_tables
23
+ @masked_column_patterns = masked_column_patterns
24
+ @default_columns = default_columns
25
+ end
26
+
27
+ def call(table:)
28
+ return blocked_result(table) if @blocked_tables.include?(table)
29
+ return not_found_result(table) unless @connection.tables.include?(table)
30
+
31
+ defaults = @default_columns[table] || []
32
+ visible = @connection.columns_for(table).reject do |col|
33
+ SqlValidator.masked_column?(col.name, @masked_column_patterns)
34
+ end
35
+ formatted = visible.map do |col|
36
+ {
37
+ name: col.name,
38
+ type: col.type.to_s,
39
+ default: defaults.empty? || defaults.include?(col.name),
40
+ }
41
+ end
42
+
43
+ Result.new(status: :ok, columns: formatted)
44
+ end
45
+
46
+ private
47
+
48
+ def blocked_result(table)
49
+ Result.new(
50
+ status: :blocked,
51
+ error_message: "Table '#{table}' is not available for querying.",
52
+ )
53
+ end
54
+
55
+ def not_found_result(table)
56
+ Result.new(
57
+ status: :not_found,
58
+ error_message: "Table '#{table}' does not exist.",
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module MysqlGenius
4
4
  module Core
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.1"
6
6
  end
7
7
  end
@@ -0,0 +1,56 @@
1
+ <!-- Explain Results -->
2
+ <div id="explain-results" class="mg-mt mg-hidden">
3
+ <div class="mg-card">
4
+ <div class="mg-card-header">
5
+ <span><strong>&#128270; EXPLAIN Output</strong></span>
6
+ <div>
7
+ <% if @ai_enabled %>
8
+ <button id="explain-optimize" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; AI Optimization</button>
9
+ <button id="explain-index-advisor" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; Index Advisor</button>
10
+ <% end %>
11
+ <button id="explain-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm">&#10005; Close</button>
12
+ </div>
13
+ </div>
14
+ <div class="mg-card-body">
15
+ <div class="mg-table-wrap">
16
+ <table class="mg-table">
17
+ <thead id="explain-thead"></thead>
18
+ <tbody id="explain-tbody"></tbody>
19
+ </table>
20
+ </div>
21
+ <div id="optimize-results" class="mg-hidden mg-mt">
22
+ <div id="optimize-content" class="mg-alert mg-alert-info"></div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <!-- Results Area -->
29
+ <div id="query-results" class="mg-mt">
30
+ <div id="results-alert" class="mg-hidden"></div>
31
+ <div id="results-stats" class="mg-mb mg-hidden">
32
+ <span id="results-row-count" class="mg-badge mg-badge-info"></span>
33
+ <span id="results-time" class="mg-badge mg-badge-secondary"></span>
34
+ <span id="results-truncated" class="mg-badge mg-badge-warning mg-hidden">Results truncated</span>
35
+ </div>
36
+ <div id="results-table-wrapper" class="mg-table-wrap mg-hidden">
37
+ <table class="mg-table">
38
+ <thead id="results-thead"></thead>
39
+ <tbody id="results-tbody"></tbody>
40
+ </table>
41
+ </div>
42
+ <div id="results-empty" class="mg-text-center mg-text-muted mg-hidden">No rows returned.</div>
43
+ </div>
44
+
45
+ <!-- AI Query Analysis Results -->
46
+ <div id="ai-query-result" class="mg-mt mg-hidden">
47
+ <div class="mg-card">
48
+ <div class="mg-card-header">
49
+ <span id="ai-query-title"><strong>&#9889; AI Analysis</strong></span>
50
+ <button id="ai-query-close" class="mg-btn mg-btn-outline-secondary mg-btn-sm">&#10005;</button>
51
+ </div>
52
+ <div class="mg-card-body">
53
+ <div id="ai-query-content" style="font-size:13px;"></div>
54
+ </div>
55
+ </div>
56
+ </div>
@@ -0,0 +1,43 @@
1
+ <!-- AI Tools Tab -->
2
+ <div class="mg-tab-content" id="tab-aitools">
3
+ <!-- Schema Review -->
4
+ <div class="mg-card mg-mb">
5
+ <div class="mg-card-header"><strong>&#9889; Schema Review</strong></div>
6
+ <div class="mg-card-body">
7
+ <div class="mg-text-muted mg-mb" style="font-size:12px;">Analyze your schema for anti-patterns: inappropriate column types, missing indexes, naming inconsistencies, and more.</div>
8
+ <div class="mg-row" style="align-items:flex-end;">
9
+ <div class="mg-col-4 mg-field">
10
+ <label for="schema-table">Table (leave blank for all)</label>
11
+ <select id="schema-table">
12
+ <option value="">All tables (top 20)</option>
13
+ <% @all_tables.each do |table| %>
14
+ <option value="<%= table %>"><%= table %></option>
15
+ <% end %>
16
+ </select>
17
+ </div>
18
+ <div class="mg-field">
19
+ <button id="schema-review-btn" class="mg-btn mg-btn-primary mg-btn-sm">&#9889; Analyze Schema</button>
20
+ </div>
21
+ </div>
22
+ <div id="schema-result" class="mg-mt mg-hidden">
23
+ <div id="schema-result-content" style="font-size:13px;"></div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <!-- Migration Risk Assessment -->
29
+ <div class="mg-card">
30
+ <div class="mg-card-header"><strong>&#9889; Migration Risk Assessment</strong></div>
31
+ <div class="mg-card-body">
32
+ <div class="mg-text-muted mg-mb" style="font-size:12px;">Paste a Rails migration or DDL and get a risk assessment: lock duration, impact on active queries, deployment strategy.</div>
33
+ <div class="mg-field">
34
+ <textarea id="migration-input" rows="8" placeholder="class AddIndexToUsers < ActiveRecord::Migration[7.0]&#10; def change&#10; add_index :users, :email, unique: true&#10; end&#10;end"></textarea>
35
+ </div>
36
+ <button id="migration-assess-btn" class="mg-btn mg-btn-primary mg-btn-sm">&#9889; Assess Risk</button>
37
+ <div id="migration-result" class="mg-mt mg-hidden">
38
+ <div id="migration-risk-badge" style="margin-bottom:8px;"></div>
39
+ <div id="migration-result-content" style="font-size:13px;"></div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
@@ -0,0 +1,95 @@
1
+ <!-- Dashboard Tab -->
2
+ <div class="mg-tab-content active" id="tab-dashboard">
3
+ <div id="dash-loading" class="mg-text-center"><span class="mg-spinner"></span> Loading dashboard...</div>
4
+ <div id="dash-error" class="mg-hidden"></div>
5
+ <div id="dash-content" class="mg-hidden">
6
+
7
+ <!-- Server Summary -->
8
+ <div class="mg-card mg-mb">
9
+ <div class="mg-card-header" style="display:flex;justify-content:space-between;align-items:center;">
10
+ <strong>Server</strong>
11
+ <button class="mg-btn mg-btn-outline-secondary mg-btn-sm dash-jump-tab" data-target="server">Details &rarr;</button>
12
+ </div>
13
+ </div>
14
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin-bottom:16px;">
15
+ <div class="mg-card">
16
+ <div class="mg-card-header"><strong>Overview</strong></div>
17
+ <div class="mg-card-body">
18
+ <div class="mg-stat-grid" id="dash-server-info"></div>
19
+ </div>
20
+ </div>
21
+ <div class="mg-card">
22
+ <div class="mg-card-header"><strong>Connections</strong></div>
23
+ <div class="mg-card-body">
24
+ <div id="dash-conn-bar" style="margin-bottom:8px;"></div>
25
+ <div class="mg-stat-grid" id="dash-conn-info"></div>
26
+ </div>
27
+ </div>
28
+ <div class="mg-card">
29
+ <div class="mg-card-header"><strong>InnoDB Buffer Pool</strong></div>
30
+ <div class="mg-card-body">
31
+ <div id="dash-innodb-bar" style="margin-bottom:8px;"></div>
32
+ <div class="mg-stat-grid" id="dash-innodb-info"></div>
33
+ </div>
34
+ </div>
35
+ <div class="mg-card">
36
+ <div class="mg-card-header"><strong>Query Activity</strong></div>
37
+ <div class="mg-card-body">
38
+ <div class="mg-stat-grid" id="dash-query-info"></div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- Top 5 Slow Queries -->
44
+ <div class="mg-card mg-mb">
45
+ <div class="mg-card-header" style="display:flex;justify-content:space-between;align-items:center;">
46
+ <strong>Slow Queries</strong>
47
+ <button class="mg-btn mg-btn-outline-secondary mg-btn-sm dash-jump-tab" data-target="slow">View all &rarr;</button>
48
+ </div>
49
+ <div class="mg-card-body">
50
+ <div id="dash-slow-empty" class="mg-text-muted mg-hidden"></div>
51
+ <div id="dash-slow-table" class="mg-table-wrap mg-hidden">
52
+ <table class="mg-table">
53
+ <thead><tr><th style="width:100px">Duration</th><th style="width:160px">Time</th><th>SQL</th></tr></thead>
54
+ <tbody id="dash-slow-tbody"></tbody>
55
+ </table>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Top 5 Expensive Queries -->
61
+ <div class="mg-card mg-mb">
62
+ <div class="mg-card-header" style="display:flex;justify-content:space-between;align-items:center;">
63
+ <strong>Most Expensive Queries</strong>
64
+ <button class="mg-btn mg-btn-outline-secondary mg-btn-sm dash-jump-tab" data-target="qstats">View all &rarr;</button>
65
+ </div>
66
+ <div class="mg-card-body">
67
+ <div id="dash-qstats-empty" class="mg-text-muted mg-hidden"></div>
68
+ <div id="dash-qstats-error" class="mg-hidden"></div>
69
+ <div id="dash-qstats-table" class="mg-table-wrap mg-hidden">
70
+ <table class="mg-table">
71
+ <thead><tr><th>Query</th><th style="text-align:right">Calls</th><th style="text-align:right">Total Time</th><th style="text-align:right">Avg Time</th></tr></thead>
72
+ <tbody id="dash-qstats-tbody"></tbody>
73
+ </table>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Index Alerts -->
79
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;">
80
+ <div class="mg-card dash-jump-tab" data-target="indexes" style="cursor:pointer;">
81
+ <div class="mg-card-body mg-text-center">
82
+ <div id="dash-dup-count" style="font-size:24px;font-weight:700;">--</div>
83
+ <div class="mg-text-muted">Duplicate Indexes</div>
84
+ </div>
85
+ </div>
86
+ <div class="mg-card dash-jump-tab" data-target="unused" style="cursor:pointer;">
87
+ <div class="mg-card-body mg-text-center">
88
+ <div id="dash-unused-count" style="font-size:24px;font-weight:700;">--</div>
89
+ <div class="mg-text-muted">Unused Indexes</div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ </div>
95
+ </div>