Skip to content

Commit 7de90b7

Browse files
committed
Merge remote-tracking branch 'origin/main' into dashboard-progressive-rendering
# Conflicts: # agent/skills/dashboard-design/SKILL.md
2 parents 54df06e + 5e08481 commit 7de90b7

39 files changed

Lines changed: 958 additions & 292 deletions

.github/workflows/codeql.yml

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ jobs:
1818
analyze:
1919
name: analyze (${{ matrix.language }})
2020
runs-on: ubuntu-latest
21-
# Code scanning is free on public repositories; on a private one it requires
22-
# GitHub Advanced Security, which this organisation's free plan does not
23-
# include. Without this guard every run fails with a 403 until the repository
24-
# is public, and a permanently red workflow trains people to stop reading CI.
25-
# DELETE THIS LINE once the repository is public — from then on a failure
26-
# here is a real finding rather than a billing state.
27-
if: github.event.repository.visibility == 'public'
21+
# No visibility guard here, deliberately. A job-level `if:` is evaluated
22+
# before the matrix expands, so a skipped run reports a single check under
23+
# the raw template name — `analyze (${{ matrix.language }})` — rather than
24+
# the two expanded names. That phantom name reached the `Protect main branch`
25+
# ruleset as a required check (GitHub's suggestion list offers whatever was
26+
# last reported), and once this repository went public and the job began
27+
# reporting the real names instead, nothing was left to satisfy the phantom:
28+
# every pull request stalled on "Expected — waiting for status to be
29+
# reported", unmergeable, with no failing job to point at.
30+
#
31+
# Code scanning is free while this repository is public. If it ever goes
32+
# private without GitHub Advanced Security, drop these contexts from the
33+
# ruleset — do not reintroduce an `if:` here.
2834
permissions:
2935
# Required to upload results to the code-scanning API.
3036
security-events: write

AGENTS.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,11 @@ only covers cloud-specific, non-obvious caveats.
199199
hardcodes `ENCRYPTION_KEYS=${ENCRYPTION_KEYS:}`; with the OS env var unset this is a
200200
circular placeholder reference that fails `EncryptionService` bean creation at boot. The
201201
local `.env` sets `ENCRYPTION_KEYS=<id>:<base64key>` matching `ENCRYPTION_KEY_ID`.
202-
- **`SECURITY_AUTH_ENABLED=false`** (set in `.env`) enables the dev auto-admin bypass, so the
203-
web UI needs no login. Auth defaults to ON in every profile otherwise (there is no
204-
`admin/admin`); a real login needs the localhost admin-bootstrap flow (see README).
202+
- **`SECURITY_AUTH_ENABLED`** defaults to ON. This Cloud VM’s `.env` sets it
203+
`true` and uses a real admin user (`admin@localhost` — create via localhost
204+
bootstrap if missing; see `CLAUDE.md`). `SECURITY_AUTH_ENABLED=false` only
205+
bypasses JWT/MCP token *validation*; it does not skip the login form or mint
206+
an admin. Dev credentials are never `admin/admin`.
205207
- **The `scheduled_tasks` table and the `vector`/`pg_stat_statements` extensions** come from
206208
`docker/postgres/init/*.sql`. In the native (non-Docker) setup those were applied by hand;
207209
they persist in the snapshot. If you ever recreate the vault DB, re-apply
@@ -242,6 +244,21 @@ only covers cloud-specific, non-obvious caveats.
242244
over `~/.hermes/profiles/u-<user>/config.yaml` and re-POST `/provision`. Symptom
243245
of a bad profile: Hermes logs `Missed model deployment` and CLI agent returns
244246
empty / “ended before producing an answer”.
247+
- **After rotating MCP tokens, restart Hermes webui (and ensure the provisioner is
248+
current).** `scripts/local-agent-provisioner.py` writes `DEEPSQL_TOKEN_FILE` +
249+
`DEEPSQL_AUTH_TOKEN` into the profile; an old long-lived provisioner process will
250+
skip the token-file path. Even with a fresh profile config, Hermes webui can keep
251+
a stale MCP subprocess env (no auth token, `DEEPSQL_MCP_USER_ID=deepsql-agent`).
252+
Symptom: Agent tab tools return `Unauthorized - Please login` while
253+
`/api/agent/session` reports `mcpAuthOk: true`. Fix: restart
254+
`scripts/local-agent-provisioner.py`, re-open Agent (re-provision), restart the
255+
Hermes webui on `:8787`, and sync default `~/.hermes/config.yaml`
256+
`mcp_servers.deepsql.env` from the active `u-<user>` profile if the shared MCP
257+
is what webui spawns.
258+
- **Multi-schema fixture.** This VM’s Postgres also has an ACME-style DB with
259+
non-`public` schemas (`crm`, `sales`, `finance`, `hr`, `inventory`) for Brain /
260+
MCP cross-schema checks. Prefer schema-qualified SQL (`sales.orders`); bare
261+
names follow the role’s `search_path` (usually `public`).
245262
- **`AGENT_WEBUI_URL` for native runs.** Default is `http://deepsql-agent:8787`
246263
(Compose DNS). Native local must set `AGENT_WEBUI_URL=http://127.0.0.1:8787` in
247264
`.env` or CLI/Slack `AgentChatClient` cannot reach the agent API.

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ backend/
109109
src/ # Frontend (React)
110110
components/ # UI components
111111
tabs/ # 40+ specialized tabs
112+
sections/ # Top-level sidebar destinations (Agent, Dashboards, Brain,
113+
# Performance = Slow Queries + Workload, Editor, Docs)
112114
lib/
113115
api/client.js # Centralized API layer (axios, 25+ modules)
114116
stores/ # Zustand stores (dashboard, connection, chat, UI)

agent/skills/dashboard-design/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Hard rules:
5757
## Procedure
5858

5959
1. **Ground.** `get_brain_context`, `get_schema`, `list_business_rules`, `get_relationships`. Obey business rules about which table/column/filter/currency a concept uses — quote them; don't guess a similar-looking table.
60-
2. **Design.** Decide the KPIs, charts, tables, and controls (date range, dropdowns) the request calls for. Give each one a short, stable, kebab-case widget id (e.g. `revenue-trend`) — you'll use the same id in the shell's slot and that widget's own block. Sketch the SQL for each — table-qualified, read-only.
60+
2. **Design.** Decide the KPIs, charts, tables, and controls (date range, dropdowns) the request calls for. Give each one a short, stable, kebab-case widget id (e.g. `revenue-trend`) — you'll use the same id in the shell's slot and that widget's own block. Sketch the SQL for each — **schema-qualified** (`crm.orders`, not bare `orders` when the DB has multiple schemas), table-qualified columns, read-only.
6161
3. **Handle dates correctly.** Check the column's type in the schema. If it's a real DATE/DATETIME, filter with `BETWEEN '2026-07-01' AND '2026-07-08'`. **If it's a Unix-epoch integer** (seconds), filter on the epoch: `col >= UNIX_TIMESTAMP('2026-07-01 00:00:00') AND col < UNIX_TIMESTAMP('2026-07-09 00:00:00')`. Build these strings in JS from the picker's values.
6262
4. **Verify each widget's query BEFORE emitting that widget's block.** Run it with `execute_sql` and READ the rows: date windows bounded and inside range (never the future), KPI value types right (name = text, money = currency), totals plausible vs a `COUNT(*)`. Fix and re-run until correct — only then emit that widget.
6363
5. **Intent checklist.** Before emitting the shell, list every explicit ask (each chart, each metric, each control like "a date range picker defaulting to today") and confirm your planned widgets cover ALL of them.

backend/src/main/java/com/dbaagent/controller/SchemaController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ public ResponseEntity<Map<String, Object>> executeQuery(
260260
}
261261
}
262262

263-
@GetMapping("/tables/{tableName}/indexes")
263+
// `{tableName:.+}` keeps schema-qualified ids (`crm.orders`) as one segment.
264+
@GetMapping("/tables/{tableName:.+}/indexes")
264265
public ResponseEntity<Map<String, Object>> getTableIndexes(
265266
@PathVariable String connectionId,
266267
@PathVariable String tableName) {
@@ -292,7 +293,7 @@ public ResponseEntity<Map<String, Object>> getTableIndexes(
292293
}
293294
}
294295

295-
@GetMapping("/tables/{tableName}/stats")
296+
@GetMapping("/tables/{tableName:.+}/stats")
296297
public ResponseEntity<Map<String, Object>> getTableStats(
297298
@PathVariable String connectionId,
298299
@PathVariable String tableName) {

backend/src/main/java/com/dbaagent/provider/mysql/MySQLIntrospectionProvider.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ public List<TableIndex> getTableIndexes(Connection connection, String database,
153153
List<TableIndex> indexes = new ArrayList<>();
154154
Map<String, TableIndex> indexMap = new HashMap<>();
155155

156+
String schemaName = database;
157+
String bareName = tableName;
158+
if (tableName != null) {
159+
int dot = tableName.lastIndexOf('.');
160+
if (dot > 0) {
161+
schemaName = tableName.substring(0, dot);
162+
bareName = tableName.substring(dot + 1);
163+
}
164+
}
165+
156166
String query = """
157167
SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE, INDEX_TYPE, SEQ_IN_INDEX
158168
FROM INFORMATION_SCHEMA.STATISTICS
@@ -161,8 +171,8 @@ public List<TableIndex> getTableIndexes(Connection connection, String database,
161171
""";
162172

163173
try (PreparedStatement stmt = connection.prepareStatement(query)) {
164-
stmt.setString(1, database);
165-
stmt.setString(2, tableName);
174+
stmt.setString(1, schemaName);
175+
stmt.setString(2, bareName);
166176

167177
try (ResultSet rs = stmt.executeQuery()) {
168178
while (rs.next()) {

backend/src/main/java/com/dbaagent/provider/postgres/PostgresIntrospectionProvider.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,18 @@ public List<TableIndex> getTableIndexes(Connection connection, String database,
204204
List<TableIndex> indexes = new ArrayList<>();
205205
Map<String, TableIndex> indexMap = new HashMap<>();
206206

207+
// Accept bare `orders` or qualified `crm.orders` so multi-schema UIs
208+
// don't silently merge indexes from every schema that shares the name.
209+
String schemaName = null;
210+
String bareName = tableName;
211+
if (tableName != null) {
212+
int dot = tableName.lastIndexOf('.');
213+
if (dot > 0) {
214+
schemaName = tableName.substring(0, dot);
215+
bareName = tableName.substring(dot + 1);
216+
}
217+
}
218+
207219
String query = """
208220
SELECT
209221
i.relname AS index_name,
@@ -212,16 +224,21 @@ public List<TableIndex> getTableIndexes(Connection connection, String database,
212224
ix.indisprimary AS is_primary,
213225
am.amname AS index_type
214226
FROM pg_class t
227+
JOIN pg_namespace n ON n.oid = t.relnamespace
215228
JOIN pg_index ix ON t.oid = ix.indrelid
216229
JOIN pg_class i ON i.oid = ix.indexrelid
217230
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
218231
JOIN pg_am am ON i.relam = am.oid
219-
WHERE t.relname = ?
232+
WHERE t.relkind IN ('r', 'p', 'm', 'v')
233+
AND t.relname = ?
234+
AND (?::text IS NULL OR n.nspname = ?)
220235
ORDER BY i.relname, a.attnum
221236
""";
222237

223238
try (PreparedStatement stmt = connection.prepareStatement(query)) {
224-
stmt.setString(1, tableName);
239+
stmt.setString(1, bareName);
240+
stmt.setString(2, schemaName);
241+
stmt.setString(3, schemaName);
225242

226243
try (ResultSet rs = stmt.executeQuery()) {
227244
while (rs.next()) {

backend/src/main/java/com/dbaagent/service/DatabaseAdvisorService.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
281281

282282
try (Connection connection = connectionService.getConnection(connectionId, connRequest)) {
283283

284-
// Query 1: Tables with high sequential scans
284+
// Query 1: Tables with high sequential scans (all non-system schemas)
285285
String query1 = """
286286
SELECT
287287
schemaname,
@@ -296,7 +296,7 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
296296
ELSE 0
297297
END as avg_seq_tup_read
298298
FROM pg_stat_user_tables
299-
WHERE schemaname = 'public'
299+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
300300
AND seq_scan > 1000
301301
AND n_live_tup > 10000
302302
AND (idx_scan IS NULL OR seq_scan > idx_scan * 2)
@@ -308,11 +308,13 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
308308
ResultSet rs = stmt.executeQuery(query1)) {
309309

310310
while (rs.next()) {
311+
String schemaName = rs.getString("schemaname");
311312
String tableName = rs.getString("tablename");
312313
long seqScans = rs.getLong("seq_scan");
313314
long seqTupRead = rs.getLong("seq_tup_read");
314315
long liveRows = rs.getLong("n_live_tup");
315316
double avgSeqRead = rs.getDouble("avg_seq_tup_read");
317+
String qualifiedTable = "public".equals(schemaName) ? tableName : schemaName + "." + tableName;
316318

317319
// Get candidate columns
318320
List<String> candidateColumns = getPostgresCandidateColumns(
@@ -325,7 +327,7 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
325327
.id(UUID.randomUUID().toString())
326328
.connectionId(connectionId)
327329
.tableName(tableName)
328-
.schemaName("public")
330+
.schemaName(schemaName)
329331
.columns(candidateColumns)
330332
.indexType("BTREE")
331333
.priority(seqScans > 10000 ?
@@ -334,13 +336,13 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
334336
.reasoning(String.format(
335337
"Table '%s' has %,d sequential scans reading %,d rows (avg %.0f rows/scan). " +
336338
"Current row count: %,d. An index would significantly improve query performance.",
337-
tableName, seqScans, seqTupRead, avgSeqRead, liveRows
339+
qualifiedTable, seqScans, seqTupRead, avgSeqRead, liveRows
338340
))
339341
.suggestedSQL(String.format(
340342
"CREATE INDEX CONCURRENTLY idx_%s_%s ON %s(%s)",
341343
tableName,
342344
String.join("_", candidateColumns),
343-
tableName,
345+
qualifiedTable,
344346
String.join(", ", candidateColumns)
345347
))
346348
.metrics(IndexRecommendation.IndexRecommendationMetrics.builder()
@@ -358,9 +360,10 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
358360
}
359361
}
360362

361-
// Query 2: Foreign keys without indexes
363+
// Query 2: Foreign keys without indexes (all non-system schemas)
362364
String query2 = """
363365
SELECT
366+
tc.table_schema,
364367
tc.table_name,
365368
kcu.column_name,
366369
ccu.table_name AS foreign_table_name
@@ -371,11 +374,11 @@ private List<IndexRecommendation> detectPostgresMissingIndexes(String connection
371374
JOIN information_schema.constraint_column_usage AS ccu
372375
ON ccu.constraint_name = tc.constraint_name
373376
WHERE tc.constraint_type = 'FOREIGN KEY'
374-
AND tc.table_schema = 'public'
377+
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
375378
AND NOT EXISTS (
376379
SELECT 1
377380
FROM pg_indexes
378-
WHERE schemaname = 'public'
381+
WHERE schemaname = tc.table_schema
379382
AND tablename = tc.table_name
380383
AND indexdef LIKE '%' || kcu.column_name || '%'
381384
)
@@ -385,15 +388,17 @@ AND NOT EXISTS (
385388
ResultSet rs = stmt.executeQuery(query2)) {
386389

387390
while (rs.next()) {
391+
String schemaName = rs.getString("table_schema");
388392
String tableName = rs.getString("table_name");
389393
String columnName = rs.getString("column_name");
390394
String foreignTable = rs.getString("foreign_table_name");
395+
String qualifiedTable = "public".equals(schemaName) ? tableName : schemaName + "." + tableName;
391396

392397
IndexRecommendation rec = IndexRecommendation.builder()
393398
.id(UUID.randomUUID().toString())
394399
.connectionId(connectionId)
395400
.tableName(tableName)
396-
.schemaName("public")
401+
.schemaName(schemaName)
397402
.columns(Collections.singletonList(columnName))
398403
.indexType("BTREE")
399404
.priority(IndexRecommendation.RecommendationPriority.HIGH)
@@ -404,7 +409,7 @@ AND NOT EXISTS (
404409
))
405410
.suggestedSQL(String.format(
406411
"CREATE INDEX CONCURRENTLY idx_%s_%s ON %s(%s)",
407-
tableName, columnName, tableName, columnName
412+
tableName, columnName, qualifiedTable, columnName
408413
))
409414
.metrics(IndexRecommendation.IndexRecommendationMetrics.builder()
410415
.estimatedImprovementPercent(70)

0 commit comments

Comments
 (0)