Skip to content

Remove legacy bootstrap surfaces#36

Merged
jruszo merged 5 commits intomasterfrom
feature/spa-bootstrap-parity
Apr 27, 2026
Merged

Remove legacy bootstrap surfaces#36
jruszo merged 5 commits intomasterfrom
feature/spa-bootstrap-parity

Conversation

@jruszo
Copy link
Copy Markdown
Owner

@jruszo jruszo commented Apr 25, 2026

Summary

  • Add SPA replacements for inventory data dictionary, instance operations, and audit visibility
  • Remove retired SQL optimization, Binlog/My2SQL, SchemaSync, and DBA principles pages, routes, templates, and permissions
  • Update seeded permissions, API surfaces, and frontend navigation to support the SPA-only flow
  • Add tests for the new APIs and end-to-end SPA parity coverage

Testing

  • Backend unit tests covering the new audit, inventory, and instance-operations APIs
  • Frontend UI and Playwright coverage for the migrated SPA workflows
  • Migration drift check and backend parity validation passed

Summary by CodeRabbit

  • New Features

    • SPA-based Audit, Data Dictionary, and Instance Operations (databases, accounts, parameters, diagnostics) with export/download and session diagnostics.
  • Bug Fixes

    • Prevented permission-related runtime errors across many UI pages; improved permission checks and error handling.
  • Removals

    • Retired legacy SQL optimization/binlog/My2SQL/SchemaSync/slow-query review UIs, plugins, charts, and related config entries.
  • Documentation

    • Updated README matrix and added SPA bootstrap parity checklist.
  • Tests

    • Added comprehensive backend API tests and a Playwright E2E SPA parity suite.
  • Refactor

    • Navigation/menu updated; server-rendered pages redirect to SPA routes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

📝 Walkthrough

Walkthrough

Removes legacy SQL analysis/optimization/binlog/schema-sync tooling and Bootstrap templates; prunes related permissions and init SQL; adds SPA-backed Audit, Data Dictionary, and Instance Operations features with new backend APIs/serializers/views/urls, frontend feature modules/pages, API clients, tests, and multiple frontend permission-check safety fixes.

Changes

Cohort / File(s) Summary
Backend: retired tooling & handlers
sql/plugins/sqladvisor.py, sql/plugins/soar.py, sql/plugins/my2sql.py, sql/plugins/schemasync.py, sql/binlog.py, sql/slowlog.py, sql/sql_analyze.py, sql/sql_optimize.py
Deleted plugin implementations and server handlers for SQLAdvisor/Soar/My2SQL/SchemaSync/binlog/slowlog/SQL analyze/optimize features.
Backend: SPA redirects & surface changes
sql/views.py, sql/urls.py, sql_api/urls.py, api_admin/settings.py
Replaced many server-rendered pages with HttpResponseRedirects to SPA routes; removed retired URL patterns and pruned system settings schema keys for retired tooling.
Backend: Audit API
api_audit/__init__.py, api_audit/serializers.py, api_audit/views.py, api_audit/urls.py, api_audit/tests.py
Added audit serializers, ListAPIView views, URL routes, date-range helper, permission checks, and coverage tests for general/query/sql-workflow/workflow-log endpoints.
Backend: Instance Operations & Data Dictionary APIs
api_instances/serializers.py, api_instances/views.py, api_instances/urls.py, api_instances/tests.py
Added many serializers, DRF views, helpers, routing, and a large test suite implementing data dictionary browsing/export and instance operations (databases/accounts/params/diagnostics).
Frontend: feature modules & pages
frontend/src/features/audit/..., frontend/src/features/instance-operations/..., frontend/src/features/inventory/..., frontend/src/app/feature-registry.ts, frontend/src/lib/api.ts
Introduced Audit, Instance Operations, and Data Dictionary feature manifests, pages/components, typed API clients, and registered modules in feature registry and API barrels.
Frontend: permission-check fixes
frontend/src/features/*/pages/*.vue (multiple files)
Added optional chaining to currentUser.permissions checks across many components to prevent runtime errors when permissions are missing.
Frontend: E2E & docs
frontend/tests/e2e/spa-parity.spec.ts, docs/spa-bootstrap-parity-checklist.md
Added Playwright SPA-parity e2e spec and a migration checklist documenting steps to retire the legacy Bootstrap frontend.
Templates removed (legacy UI)
sql/templates/... (data_dictionary.html, database.html, param.html, dbdiagnostic.html, my2sql.html, sqladvisor.html, sqlanalyze.html, slowquery.html, schemasync.html, audit*.html, dbaprinciples.html)
Removed server-rendered Bootstrap templates and embedded client scripts for retired tooling/pages.
Permissions, fixtures, migrations
sql/models.py, sql/fixtures/auth_group.sql, sql/local_demo.py, sql/migrations/0015_alter_permission_options.py
Removed retired permission tuples, adjusted seeded/demo permission sets, and added migration to remove/recreate retired permissions.
Init SQL / scripts / packaging / chart changes
src/init_sql/*, src/plugins/soar.yaml, src/docker*, src/charts/*, requirements.txt, admin.sh, src/docker/setup.sh
Removed many legacy init SQL files and tooling configs, deleted SOAR/sqladvisor/my2sql install steps, removed mysql-replication pip dep, and adjusted Docker/chart provisioning.
Dashboard / charts DAO / UI
common/utils/chart_dao.py, api_admin/dashboard.py, common/dashboard.py, common/templates/dashboard.html, frontend/src/features/dashboard/pages/HomePage.vue
Removed slow-query aggregation DAO methods, corresponding chart data from dashboard payloads, and UI chart elements.
Tests updated/removed
sql/tests.py, sql/test_notify.py, sql/plugins/tests.py, others
Removed tests tied to deleted tooling; adapted plugin tests to generic Plugin behavior and updated view tests to expect SPA redirects.
Small UI/config edits
common/templates/base.html, common/templates/config.html, frontend/src/features/settings/system-settings.ts
Updated sidebar/navigation and config UI to remove retired tooling entries and reflect new SPA routes.
Misc: small utils docs
sql/utils/sql_utils.py, api_admin/dashboard.py
Minor docstring and chart payload adjustments aligning docs with implementation and removed chart series.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser (SPA)
    participant AuditPage as AuditPage (Vue)
    participant FrontAPI as Frontend API (lib/api)
    participant Backend as Django API
    participant DB as Metadata DB

    Browser->>AuditPage: navigate /audit
    AuditPage->>FrontAPI: fetchGeneralAuditLogs(filters, token)
    FrontAPI->>Backend: GET /api/v1/audit/general/?...
    Backend->>DB: query Audit models with filters and date range
    DB-->>Backend: rows + count
    Backend-->>FrontAPI: JSON {count, results}
    FrontAPI-->>AuditPage: parsed response
    AuditPage->>Browser: render audit table
Loading
sequenceDiagram
    participant Browser as Browser (SPA)
    participant OpsPage as Instance Ops Page
    participant FrontAPI as Frontend API (lib/api)
    participant Backend as Django API
    participant Engine as Remote DB Engine
    participant MetaDB as Metadata DB

    Browser->>OpsPage: open Account Management
    OpsPage->>FrontAPI: fetchInstanceOperationAccounts(instanceId, token)
    FrontAPI->>Backend: GET /api/v1/instance-operations/account/?instance_id=...
    Backend->>MetaDB: read saved account metadata
    MetaDB-->>Backend: account records
    Backend-->>FrontAPI: JSON account list
    FrontAPI-->>OpsPage: show accounts
    Browser->>OpsPage: submit create account form
    OpsPage->>FrontAPI: createInstanceOperationAccount(payload, token)
    FrontAPI->>Backend: POST /api/v1/instance-operations/account/
    Backend->>Engine: connect & EXECUTE CREATE/GRANT SQL
    Engine-->>Backend: success
    Backend->>MetaDB: persist metadata
    MetaDB-->>Backend: saved record
    Backend-->>FrontAPI: success response
    FrontAPI-->>OpsPage: refresh accounts, show success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I nudged the old templates out, packed plugins in a sack,

Brought logs and dictionaries to town and let the SPA lead back.
Permissions now are gentle, no more crashes late at night,
Engines hum, tests hop by, and routes all feel just right.
A tidy thump, a happy hop — the codebase gleams and bright.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/spa-bootstrap-parity

Copy link
Copy Markdown

@github-advanced-security github-advanced-security AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (19)
sql/migrations/0015_alter_permission_options.py (1)

95-95: Consider a non-noop reverse for the data migration.

migrations.RunPython.noop as the reverse means rolling back this migration to 0014_mailboxitem will leave the retired permissions deleted (the AlterModelOptions reverse won't recreate auth.Permission rows either, since those are populated by the post-migrate signal only for permissions present in the current Meta.permissions). That's likely fine given the feature removal is intentional, but worth confirming you don't rely on a clean rollback restoring them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/migrations/0015_alter_permission_options.py` at line 95, The data
migration currently uses migrations.RunPython(remove_retired_sql_permissions,
migrations.RunPython.noop) which leaves deleted auth.Permission rows
unrecoverable on rollback; replace the noop with a proper reverse function
(e.g., a recreate_retired_sql_permissions undo) that recreates the removed
Permission rows (using the same codename, content_type and name used by
remove_retired_sql_permissions) or explicitly document/confirm that rollbacks
must not rely on restoring those rows; update the RunPython call to
migrations.RunPython(remove_retired_sql_permissions,
recreate_retired_sql_permissions) so rollbacks return the DB to the
pre-migration state.
frontend/src/lib/api.ts (1)

1457-1475: Falsy-zero guard on workflow_type.

if (options.workflow_type) will skip workflow_type === 0. If 0 is ever a meaningful workflow type for the audit endpoint (e.g. "unknown"/"other"), this filter would silently drop it. Same goes for audit_id/workflow_id if they can ever legitimately be 0. Switching to != null (or explicit !== undefined) is safer.

♻️ Defensive refinement
-  if (options.audit_id) {
+  if (options.audit_id != null) {
     params.set('audit_id', `${options.audit_id}`)
   }
-  if (options.workflow_id) {
+  if (options.workflow_id != null) {
     params.set('workflow_id', `${options.workflow_id}`)
   }
-  if (options.workflow_type) {
+  if (options.workflow_type != null) {
     params.set('workflow_type', `${options.workflow_type}`)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/lib/api.ts` around lines 1457 - 1475, The guard in
fetchWorkflowOperationAuditLogs currently uses truthy checks (if
(options.audit_id), if (options.workflow_id), if (options.workflow_type)) which
will skip legitimate zero values; update each guard to check for presence rather
than truthiness (e.g., use options.audit_id != null or options.audit_id !==
undefined, and the same for workflow_id and workflow_type) so 0 is included in
the URLSearchParams when provided; keep the rest of the function (building
params and calling apiGet/extractData) unchanged.
sql/tests.py (1)

190-190: Optional: drop f prefix on placeholder-less strings (Ruff F541).

These GET URLs are pre-existing but flagged by Ruff in the touched test methods. Cleaning them up while you’re here keeps lint clean.

♻️ Suggested cleanup
-        r = self.client.get(f"/database/", data=data)
+        r = self.client.get("/database/", data=data)
@@
-        r = self.client.get(f"/dbdiagnostic/", data=data)
+        r = self.client.get("/dbdiagnostic/", data=data)
@@
-        r = self.client.get(f"/instanceparam/", data=data)
+        r = self.client.get("/instanceparam/", data=data)
@@
-        r = self.client.get(f"/audit_sqlquery/", data=data)
+        r = self.client.get("/audit_sqlquery/", data=data)
@@
-        r = self.client.get(f"/audit_sqlworkflow/", data=data)
+        r = self.client.get("/audit_sqlworkflow/", data=data)

Also applies to: 198-198, 206-206, 238-238, 244-244

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/tests.py` at line 190, Remove unnecessary f-string prefixes from literal
URLs in the tests: replace calls like self.client.get(f"/database/", data=data)
with self.client.get("/database/", data=data) (and similarly for the other
occurrences flagged by Ruff). Locate the self.client.get calls in sql/tests.py
that use f-strings without any interpolation and convert them to normal string
literals so Ruff F541 is resolved.
api_instances/serializers.py (1)

218-219: Mark instance_name as read-only.

instance_name = serializers.CharField(source="instance.instance_name") is only used for output in the InstanceOperationParamHistory GET endpoint. Without read_only=True, DRF treats it as a writable field on deserialization, which is misleading and violates DRF best practices for dotted-source fields.

♻️ Proposed fix
 class InstanceParamHistorySerializer(serializers.ModelSerializer):
-    instance_name = serializers.CharField(source="instance.instance_name")
+    instance_name = serializers.CharField(
+        source="instance.instance_name", read_only=True
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/serializers.py` around lines 218 - 219, The
InstanceParamHistorySerializer defines instance_name =
serializers.CharField(source="instance.instance_name") but it's an output-only
dotted-source field; modify InstanceParamHistorySerializer to mark instance_name
as read-only by setting read_only=True on that field (e.g., instance_name =
serializers.CharField(source="instance.instance_name", read_only=True)) so DRF
won't treat it as writable during deserialization.
api_audit/urls.py (1)

5-10: Optional: Consider naming the URL patterns.

None of the routes specify a name= kwarg. If any code (or templates/tests) needs reverse() to resolve these audit endpoints, you'll need names. Adding them now is cheap insurance and aids discoverability.

♻️ Suggested addition
-    path("v1/audit/general/", views.GeneralAuditLogList.as_view()),
-    path("v1/audit/query/", views.QueryAuditLogList.as_view()),
-    path("v1/audit/sql-workflow/", views.SqlWorkflowAuditLogList.as_view()),
-    path("v1/audit/workflow-log/", views.WorkflowOperationLogList.as_view()),
+    path("v1/audit/general/", views.GeneralAuditLogList.as_view(), name="audit-general"),
+    path("v1/audit/query/", views.QueryAuditLogList.as_view(), name="audit-query"),
+    path(
+        "v1/audit/sql-workflow/",
+        views.SqlWorkflowAuditLogList.as_view(),
+        name="audit-sql-workflow",
+    ),
+    path(
+        "v1/audit/workflow-log/",
+        views.WorkflowOperationLogList.as_view(),
+        name="audit-workflow-log",
+    ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_audit/urls.py` around lines 5 - 10, Add explicit name= kwargs to each URL
pattern so reverse()-based lookups will work; update the urlpatterns entries
that reference GeneralAuditLogList, QueryAuditLogList, SqlWorkflowAuditLogList,
and WorkflowOperationLogList to include unique, descriptive names (e.g.,
"audit-general", "audit-query", "audit-sql-workflow", "audit-workflow-log")
ensuring each path(...) call includes the name parameter.
sql/views.py (1)

51-52: spa_redirect is a redundant wrapper.

The helper just forwards to HttpResponseRedirect(path) with no added behavior (no logging, no logged-out handling, no querystring forwarding). At call sites, return HttpResponseRedirect("/audit") is just as clear. Either inline it or make it carry its weight (e.g., preserve querystrings, emit a Deprecation header, or log redirect telemetry to track legacy hits during the transition).

♻️ Suggested enrichment if you want to keep the helper
-def spa_redirect(path):
-    return HttpResponseRedirect(path)
+def spa_redirect(path):
+    """Redirect a legacy server-rendered route to its SPA equivalent.
+
+    Centralized so legacy hits can be observed during the SPA migration.
+    """
+    logger.info("Legacy route redirected to SPA path: %s", path)
+    response = HttpResponseRedirect(path)
+    response["Deprecation"] = "true"
+    return response
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/views.py` around lines 51 - 52, The spa_redirect helper is
redundant—remove the spa_redirect function and replace its usages (calls to
spa_redirect(...)) with direct returns of HttpResponseRedirect(path); search for
the spa_redirect symbol and update call sites to use HttpResponseRedirect(path)
so behavior remains identical, then delete the spa_redirect definition from the
module to avoid dead code.
frontend/src/features/instance-operations/manifest.ts (1)

17-50: Differentiate the navigation icons.

All four sidebar items reuse the same Wrench icon, which makes the menu harder to scan and weakens visual differentiation between Databases, Accounts, Parameters, and Diagnostics. The corresponding pages already import distinct lucide icons (e.g., Database, UserRound/KeyRound, SlidersHorizontal, SquareActivity); reusing those here would keep the navigation consistent with each page's identity.

♻️ Suggested change
-import { Wrench } from 'lucide-vue-next'
+import { Database, SlidersHorizontal, SquareActivity, UserRound } from 'lucide-vue-next'
@@
-      icon: Wrench,
-      order: 22,
+      icon: Database,
+      order: 22,
@@
-      icon: Wrench,
-      order: 23,
+      icon: UserRound,
+      order: 23,
@@
-      icon: Wrench,
-      order: 24,
+      icon: SlidersHorizontal,
+      order: 24,
@@
-      icon: Wrench,
-      order: 25,
+      icon: SquareActivity,
+      order: 25,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/manifest.ts` around lines 17 - 50,
The navigation items in the manifest reuse the same Wrench icon making items
hard to distinguish; update the navigation array so each route uses its
page-specific lucide icon: use Database for '/instance-operations/databases',
UserRound or KeyRound for '/instance-operations/accounts' (pick the one matching
the account page import), SlidersHorizontal for
'/instance-operations/parameters', and SquareActivity for
'/instance-operations/diagnostics' instead of Wrench; ensure the corresponding
icon identifiers (Database, UserRound/KeyRound, SlidersHorizontal,
SquareActivity) are imported at the top of the file and referenced in the
navigation entries.
api_instances/urls.py (1)

17-120: Routes look good; consider adding name= kwargs.

The new data-dictionary/* and instance-operations/* routes are consistent and won't clash with the earlier v1/instance/<int:pk>/ (the int converter only matches digits). Same nit as the audit URLs: none of the new paths declare a name=, which makes reverse() and named-URL referencing impossible. Adding names per route would future-proof the API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/urls.py` around lines 17 - 120, The routes lack name= kwargs
which prevents reverse() and named URL lookup; update each path(...) entry
(e.g., the ones referencing DataDictionaryInstanceList,
DataDictionaryDatabaseList, DataDictionaryTableList, DataDictionaryTableDetail,
DataDictionaryExport, InstanceOperationDatabaseListCreate,
InstanceOperationDatabaseInstanceList, InstanceOperationDatabaseDetail,
InstanceOperationAccountListCreate, InstanceOperationAccountInstanceList,
InstanceOperationAccountMetadata, InstanceOperationAccountPassword,
InstanceOperationAccountLock, InstanceOperationAccountDelete,
InstanceOperationAccountGrant, InstanceOperationParamList,
InstanceOperationParamInstanceList, InstanceOperationParamHistory,
InstanceOperationParamEdit, InstanceOperationDiagnosticInstanceList,
InstanceOperationDiagnosticProcessList, InstanceOperationDiagnosticKillPreview,
InstanceOperationDiagnosticKill, InstanceOperationDiagnosticTablespace,
InstanceOperationDiagnosticTransactions, InstanceOperationDiagnosticLocks) to
include a unique name="..." kwarg; pick consistent, descriptive names (e.g.,
"data_dictionary.instances.list", "instance_operations.database.create" or
similar) and ensure names are unique across the module so reverse("name") can be
used reliably.
frontend/src/features/audit/pages/AuditPage.vue (1)

158-186: Double-fetch on view switch.

setView() mutates filters.action, filters.status, filters.syntaxType and immediately calls loadAuditLogs(). Those same fields are watched at lines 181–186, so the watcher additionally schedules debouncedLoadAuditLogs() (~250 ms later), producing a second request per tab switch — wasteful and easy to mistake for a flicker/pagination bug. Either reset the filters before the watcher is registered, suppress the debounce when switching views, or skip the explicit loadAuditLogs() call in setView() and rely on the watcher.

♻️ One way to dedupe
 function setView(view: AuditView) {
   activeView.value = view
   filters.page = 1
   filters.action = ''
   filters.status = ''
   filters.syntaxType = ''
-  void loadAuditLogs()
+  // The watcher will pick up the filter resets and trigger a debounced load.
+  // Force an immediate load only when nothing changed (i.e., no other watcher fire).
+  void loadAuditLogs()
+  // ...or, alternatively, drop this explicit call and let the watcher run.
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/audit/pages/AuditPage.vue` around lines 158 - 186,
setView currently mutates filters.action/status/syntaxType and calls
loadAuditLogs(), but those same filter fields are watched by the watcher that
triggers debouncedLoadAuditLogs(), causing a duplicate fetch; to fix, remove the
explicit loadAuditLogs() call from setView and rely on the watcher (or
alternatively cancel/suppress the debounce when switching views), i.e. update
the setView function (referencing setView, filters, loadAuditLogs,
debouncedLoadAuditLogs, and the watch callback) so only one request is scheduled
when switching views.
frontend/src/features/instance-operations/pages/DatabaseManagementPage.vue (1)

247-251: Refresh button only checks loadingDatabases.

refreshDatabases() awaits both loadInstances() and loadDatabases(), but the button is only disabled when loadingDatabases is true. A user clicking during the loadInstances phase can stack overlapping refresh calls.

-      <Button variant="outline" type="button" class="gap-2" :disabled="loadingDatabases" `@click`="void refreshDatabases()">
+      <Button variant="outline" type="button" class="gap-2" :disabled="loadingInstances || loadingDatabases" `@click`="void refreshDatabases()">
         <RefreshCw class="h-4 w-4" />
         Refresh
       </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/DatabaseManagementPage.vue`
around lines 247 - 251, The Refresh button only checks loadingDatabases but
refreshDatabases() awaits both loadInstances() and loadDatabases(), so update
the button disabling logic to prevent clicks while either phase is running by
using both flags (e.g., change the :disabled binding on the Refresh Button to
combine loadingDatabases and loadingInstances), and ensure the component
exposes/sets loadingInstances around the loadInstances() call so the combined
check is reliable.
api_instances/tests.py (1)

24-32: FakeDictionaryResult.to_dict() fails silently when columns and rows aren't column-aligned.

For get_all_databases() you set rows=["appdb", "mysql", "hidden"] and column_list=[], so to_dict() would iterate strings and produce [{}, {}, {}]. Not exercised by current tests, but if a future caller invokes to_dict() on that result it'll quietly return empty dicts instead of raising. Consider asserting or normalizing the row shape so the fake fails loudly when misused.

     def to_dict(self):
-        return [dict(zip(self.column_list, row)) for row in self.rows]
+        return [
+            dict(zip(self.column_list, row, strict=True))
+            for row in self.rows
+        ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/tests.py` around lines 24 - 32, FakeDictionaryResult.to_dict
currently returns empty dicts when column_list is empty or when row lengths
don't match columns; update FakeDictionaryResult.to_dict to validate and fail
loudly: if column_list is empty raise ValueError (or if column_list has length 1
allow scalar rows by normalizing to single-element tuples), and for each row
assert that its length equals len(column_list) (raising ValueError with a clear
message if not). Reference: class FakeDictionaryResult and its to_dict
method—add these checks/normalization so tests or callers get an explicit error
instead of silent empty dicts.
frontend/src/features/inventory/pages/DataDictionaryPage.vue (1)

261-269: Synchronous revokeObjectURL after click() can race in some browsers.

A handful of older browsers may not have started the download by the time revokeObjectURL fires synchronously after click(). Modern Chrome/Firefox handle this fine, but defensive coding (a microtask/setTimeout(..., 0) deferred revoke) eliminates the rare interrupted-download case.

-    anchor.click()
-    anchor.remove()
-    window.URL.revokeObjectURL(objectUrl)
+    anchor.click()
+    anchor.remove()
+    setTimeout(() => window.URL.revokeObjectURL(objectUrl), 0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/inventory/pages/DataDictionaryPage.vue` around lines
261 - 269, The synchronous call to window.URL.revokeObjectURL(objectUrl) may
race with the download start after anchor.click(); defer revocation to ensure
the browser has time to begin the download by replacing the immediate revoke
with a microtask or macrotask (e.g., queueMicrotask or setTimeout(..., 0)) so
revokeObjectURL(objectUrl) runs shortly after click; update the block that
creates objectUrl/anchor (the objectUrl variable and anchor.click() usage in
DataDictionaryPage.vue) to revoke the URL asynchronously instead of immediately.
frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue (2)

102-105: Silent column truncation at 10.

Object.keys(firstRow).slice(0, 10) quietly drops anything beyond the 10th key, so wider diagnostic payloads (some MySQL processlist/locks views have >10 columns) won't render any cue that more data is available. Either drop the slice or surface a "+N more columns" indicator so operators don't make decisions on partial data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue`
around lines 102 - 105, The current columns computed uses
Object.keys(firstRow).slice(0, 10) which silently drops columns; change it to
keep the full key list (const columns = computed(() => { const firstRow =
rows.value[0]; return firstRow ? Object.keys(firstRow) : [] })) or, if you must
limit for UI, create two computeds: displayColumns (first 10) and
extraColumnsCount (computed from Object.keys(firstRow).length -
displayColumns.length) and update the template to render displayColumns plus a
"+{extraColumnsCount} more columns" indicator; update references to columns in
the component accordingly (look for the columns computed and any template
usages).

115-129: Checkbox treats process ID 0 as "no id" and re-evaluates processId(row) three times.

processId(row) ? selectedThreadIds.includes(processId(row) as number) : false and :disabled="!processId(row)" use truthiness, so a (theoretical) thread id of 0 would render unchecked and disabled even when selected. MySQL thread IDs are positive in practice, but the same processId(row) is also called up to three times per row per render — clearer and safer to compute once and compare against null.

♻️ Suggested change
-                  <td v-if="activeTab === 'processes' && canKillProcesses" class="px-4 py-3">
-                    <input
-                      type="checkbox"
-                      class="h-4 w-4 rounded border-slate-300"
-                      :aria-label="`Select process ${processId(row) ?? 'unknown'}`"
-                      :checked="processId(row) ? selectedThreadIds.includes(processId(row) as number) : false"
-                      :disabled="!processId(row)"
-                      `@change`="toggleThread(row)"
-                    >
-                  </td>
+                  <td v-if="activeTab === 'processes' && canKillProcesses" class="px-4 py-3">
+                    <input
+                      type="checkbox"
+                      class="h-4 w-4 rounded border-slate-300"
+                      :aria-label="`Select process ${processId(row) ?? 'unknown'}`"
+                      :checked="processId(row) !== null && selectedThreadIds.includes(processId(row) as number)"
+                      :disabled="processId(row) === null"
+                      `@change`="toggleThread(row)"
+                    >
+                  </td>

Also applies to: 363-373

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue`
around lines 115 - 129, processId(row) is being called multiple times and its
truthiness treats a valid numeric id of 0 as "no id"; compute the id once and
use explicit null checks instead of truthy checks. Update the code paths that
render/disable/compare (calls around processId(row), toggleThread, and any
template bindings using processId) to call processId(row) a single time into a
const (e.g., const id = processId(row)), reuse that id in selectedThreadIds
checks and in :disabled, and change conditions to id !== null (or id == null)
rather than relying on truthiness so an id of 0 is handled correctly; also
ensure toggleThread already uses the single computed id and does not re-call
processId.
api_audit/views.py (1)

20-30: Optional: _date_range returns naive datetimes.

With Django's default USE_TZ=True, comparing a naive datetime against a timezone-aware DateTimeField raises a RuntimeWarning and (depending on the DB backend) can drop the date by a few hours near boundaries. If this project runs USE_TZ=True, wrap the parsed values with timezone.make_aware (matching the existing from django.utils import timezone already used elsewhere in the codebase). If USE_TZ=False, this is fine to leave.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_audit/views.py` around lines 20 - 30, _date_range currently returns naive
datetimes which will cause warnings/incorrect comparisons when Django is running
with USE_TZ=True; update _date_range to make the parsed start and end datetimes
timezone-aware using django.utils.timezone.make_aware (preserving the added day
on end), e.g., call timezone.make_aware on the parsed start and end before
returning (keep the existing ValueError handling and return signature).
frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue (1)

166-224: Avoid the redundant initial load triggered by both refreshAll and the watcher.

refreshAll() runs loadInstances, loadParams, and loadHistory in parallel. On a fresh mount selectedInstanceId is still null, so loadParams and loadHistory early-return. As soon as loadInstances assigns selectedInstanceId.value, the [selectedInstanceId, editableFilter] watcher fires and re-issues loadParams + loadHistory. The net effect is two extra no-op invocations on every refresh. Awaiting loadInstances first and relying on the watcher (or only fetching in refreshAll when an id is already set) keeps behavior identical with one fewer round of wasted calls.

♻️ Proposed refactor
 async function refreshAll() {
   feedback.value = ''
-  await Promise.all([loadInstances(), loadParams(), loadHistory()])
+  const previousId = selectedInstanceId.value
+  await loadInstances()
+  // If loadInstances did not change selectedInstanceId, the watcher won't fire,
+  // so trigger the dependent loads explicitly.
+  if (selectedInstanceId.value === previousId) {
+    await Promise.all([loadParams(), loadHistory()])
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue`
around lines 166 - 224, The initial mount triggers duplicate calls because
refreshAll() runs loadInstances(), loadParams(), and loadHistory() in parallel
while selectedInstanceId is still null, then the watcher on [selectedInstanceId,
editableFilter] fires when loadInstances sets selectedInstanceId; to fix, change
refreshAll() to await loadInstances() first and only then call loadParams() and
loadHistory() (e.g., await loadInstances(); await Promise.all([loadParams(),
loadHistory()])) so the watcher handles subsequent loads, or alternatively
conditionally skip calling loadParams()/loadHistory() in refreshAll() when
selectedInstanceId.value is falsy; update the refreshAll function (and keep the
watcher and onMounted flow) accordingly to eliminate the redundant early no-op
calls.
frontend/src/features/instance-operations/pages/AccountManagementPage.vue (2)

237-240: Same redundant-load pattern as ParameterSettingsPage — see that comment.

refreshAccounts() parallel-fires loadInstances and loadAccounts; the latter early-returns when selectedInstanceId is null, and then the [selectedInstanceId, savedOnly] watcher fires loadAccounts again once loadInstances assigns the id. Awaiting loadInstances first and only calling loadAccounts if the id didn't change avoids the extra no-op round trip.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/AccountManagementPage.vue`
around lines 237 - 240, refreshAccounts currently calls loadInstances and
loadAccounts in parallel which causes a redundant no-op because loadAccounts
early-returns when selectedInstanceId is null and the [selectedInstanceId,
savedOnly] watcher then calls loadAccounts again after loadInstances sets the
id; fix by making refreshAccounts call await loadInstances() first, capture the
prior selectedInstanceId before the call, and only call loadAccounts()
afterwards if selectedInstanceId is non-null and equal to the prior value (i.e.,
the instance id did not change), so the watcher handles the case where
loadInstances sets a new id.

518-705: Reclaim the right-hand column when no form is open.

The two-column grid xl:grid-cols-[minmax(0,1fr)_minmax(21rem,0.34fr)] keeps the second track sized (≥21rem) even though the form Card is gated by v-if="activeFormMode". As a result the accounts table is squeezed by ~21rem of empty space whenever a user is just browsing/filtering. Either collapse to a single column when there's no active form, or move the form into a side drawer so the table can use the full width by default — this keeps the open-on-selection behavior the layout already implements while giving back useful list space.

♻️ Sketch of dynamic grid template
-    <div class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(21rem,0.34fr)]">
+    <div
+      class="grid gap-6"
+      :class="activeFormMode ? 'xl:grid-cols-[minmax(0,1fr)_minmax(21rem,0.34fr)]' : 'xl:grid-cols-1'"
+    >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/AccountManagementPage.vue`
around lines 518 - 705, The grid's xl columns are fixed causing wasted space
when no form is open; change the container div's static class
"xl:grid-cols-[minmax(0,1fr)_minmax(21rem,0.34fr)]" to a dynamic class bound to
a computed or method (e.g. computed name gridColsClass or getGridColsClass) that
returns 'xl:grid-cols-[minmax(0,1fr)_minmax(21rem,0.34fr)]' when activeFormMode
is truthy and 'xl:grid-cols-1' (or equivalent single-column class) when falsy,
then update the div to use :class="['grid gap-6', gridColsClass]" so the
right-hand Card collapses away when no form is open (refer to the container div,
activeFormMode, and the new computed gridColsClass).
api_instances/views.py (1)

143-150: Optional: chain the re-raised ValidationError to silence Ruff B904.

Across this file (this helper plus many except Exception as exc: ... raise serializers.ValidationError(...) blocks at L729/792/888/976/1066/1128/1252/1303/1409/1464/1508/1578/1671/1816/1915/1960/2005/2053/2099/2143), Ruff flags the bare raise form. None of these are bugs, but using raise ... from exc (or from None where the engine error message is already surfaced) preserves causal chains in tracebacks and clears the lint. Worth a sweep in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/views.py` around lines 143 - 150, The ValidationError being
re-raised in helpers like _data_dictionary_instance should chain the original
exception to satisfy Ruff B904; change each except block (e.g., except
Instance.DoesNotExist:) to capture the exception (except Instance.DoesNotExist
as exc:) and re-raise the serializers.ValidationError(...) using "raise
serializers.ValidationError(... ) from exc" (or "from None" if you intentionally
want to suppress the original traceback where the lower-level error message is
already surfaced), and apply the same pattern for the other except Exception
handlers listed (lines referenced in the review) to preserve causal chains and
silence the linter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api_audit/tests.py`:
- Around line 58-78: The test test_general_audit_ignores_invalid_date_filter
uses a brittle hardcoded end_date "2026-04-25"; change that to a far-future
sentinel like "9999-12-31" so the test still verifies that _date_range (or the
audit filtering logic used by the general audit view) drops the date filter when
start_date is unparseable rather than accidentally relying on the current date;
update the end_date value in the request payload used in this test to
"9999-12-31".

In `@api_audit/views.py`:
- Around line 200-223: The GET handler currently passes the raw audit_id string
into WorkflowLog.objects.filter(audit_id=audit_id) which can cause a 500 for
non-numeric input; before the filter (in the method decorated get),
validate/coerce audit_id to an integer when it's provided (e.g., attempt
int(audit_id) and on ValueError raise serializers.ValidationError with a clear
message) and then use the integer value in
WorkflowLog.objects.filter(audit_id=audit_id); keep the existing WorkflowAudit
lookup behavior when audit_id is not provided.

In `@api_instances/serializers.py`:
- Around line 143-144: The validate_password method currently mutates input by
returning value.strip(); instead, preserve the original password and only
validate: in validate_password(value) do not call strip(), but check if value is
empty or only whitespace and raise a serializers.ValidationError (or appropriate
DRF error) for blank passwords; keep the original value unchanged and return
value when valid so stored passwords match exactly what the user provided.

In `@api_instances/views.py`:
- Around line 1789-1808: The code assumes engine.get_variables returns rows with
at least two columns when assigning runtime_value =
current_variables.rows[0][1], which can raise IndexError/TypeError for
single-element or None-padded rows; update the block handling current_variables
(after current_variables.rows check in the same try) to validate that
current_variables.rows[0] is a sequence with length >= 2 and that the second
element is present, and if not raise serializers.ValidationError({"errors":
"Parameter returned unexpected row shape from instance."}) so the error flows
through the existing ValidationError handling; reference engine.get_variables,
current_variables.rows and the runtime_value assignment to locate where to add
this guard.
- Around line 1395-1428: The password-reset path currently calls
InstanceAccount.objects.update_or_create(..., defaults={"remark": ""}) which
overwrites an existing account's remark; change it to avoid clobbering remark by
using InstanceAccount.objects.get_or_create(...) (or call update_or_create
without the remark default) so existing rows are left untouched and only
newly-created accounts get an initial remark; locate the call to
InstanceAccount.objects.update_or_create in the
InstanceOperationAccountPassword.post handler and replace it with get_or_create
(and set remark for the created case) or remove the remark from defaults to
preserve existing metadata.

In `@frontend/src/features/audit/pages/AuditPage.vue`:
- Around line 188-191: Wrap the onMounted initial load in the same
toUserFacingMessage error-handling flow so rejections from
authStore.loadCurrentUser() are surfaced; specifically, replace the bare await
authStore.loadCurrentUser() call inside the onMounted(async () => { ... }) with
an awaited toUserFacingMessage wrapper (or a try/catch that calls
toUserFacingMessage on failure) referencing authStore.loadCurrentUser(), then
proceed to call loadAuditLogs() only after successful authentication; use the
existing toUserFacingMessage helper and the existing loadAuditLogs function
names so failures are displayed to the user.

In `@frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue`:
- Around line 232-247: The onMounted block currently awaits
authStore.loadCurrentUser() with no error handling; wrap the call in the same
toUserFacingMessage flow used elsewhere (e.g., DatabaseManagementPage.vue) so
any rejection becomes a user-facing string and is assigned to error.value, then
return early to stop the permission checks and loadInstances call. Specifically,
catch errors from authStore.loadCurrentUser() (or call it through
toUserFacingMessage), set error.value to the returned message, and exit the
onMounted handler before checking canOpenDiagnostics, firstVisibleTab, setting
activeTab, or calling loadInstances.

In `@frontend/src/features/inventory/pages/DataDictionaryPage.vue`:
- Around line 277-286: Wrap the authStore.loadCurrentUser() call inside the
onMounted handler with the same error-to-user-message flow used elsewhere: call
toUserFacingMessage(...) around await authStore.loadCurrentUser() (or catch its
rejection and pass the error into toUserFacingMessage) and assign the result to
error when present, before returning; keep subsequent permission check for
canViewDataDictionary and the call to refreshDataDictionary unchanged. This
mirrors the pattern used in DatabaseManagementPage (use toUserFacingMessage to
convert loadCurrentUser errors into a user-facing string and set error.value).

In `@sql/migrations/0015_alter_permission_options.py`:
- Around line 5-24: The migration remove_retired_sql_permissions deletes
permissions listed in RETIRED_SQL_PERMISSION_CODENAMES but seed SQL files still
INSERT those same codenames, causing them to be re-created then removed; fix by
updating the seed SQL files (src/init_sql/v*.sql) to remove or conditionally
skip INSERTs for the listed codenames (menu_sqlanalyze, menu_sqloptimize,
menu_sqladvisor, menu_slowquery, menu_my2sql, menu_schemasync, menu_document,
sql_analyze, optimize_sqladvisor, optimize_sqltuning, optimize_soar), or change
initialization ordering so seeds run after this migration, and add a short note
in docs/tests indicating these codenames are retired (referencing
RETIRED_SQL_PERMISSION_CODENAMES and remove_retired_sql_permissions to locate
the migration).

In `@sql/plugins/tests.py`:
- Line 145: The test unpacks stderr from
plugin.execute_cmd(cmd_args).communicate() but never uses it, triggering Ruff
RUF059; change the unpack to ignore stderr by renaming it to _stderr or dropping
it (e.g., stdout, _stderr = plugin.execute_cmd(cmd_args).communicate() or just
stdout = plugin.execute_cmd(cmd_args).communicate()[0]) so update the call site
in tests.py where execute_cmd and communicate are used to remove the unused
variable.

In `@sql/views.py`:
- Around line 633-648: The three view functions audit, audit_sqlquery, and
audit_sqlworkflow currently all redirect to "/audit", which loses tab context;
update each to append a tab query parameter (e.g., "/audit?tab=general",
"/audit?tab=query", "/audit?tab=sql-workflow") so bookmarks retain intent, and
then update the frontend AuditPage.vue to read the route query in its
onMounted() hook: have onMounted() inspect route.query.tab (falling back to
'general'), set activeView accordingly, and only then trigger data loading so
the correct tab content is displayed on initial load.

---

Nitpick comments:
In `@api_audit/urls.py`:
- Around line 5-10: Add explicit name= kwargs to each URL pattern so
reverse()-based lookups will work; update the urlpatterns entries that reference
GeneralAuditLogList, QueryAuditLogList, SqlWorkflowAuditLogList, and
WorkflowOperationLogList to include unique, descriptive names (e.g.,
"audit-general", "audit-query", "audit-sql-workflow", "audit-workflow-log")
ensuring each path(...) call includes the name parameter.

In `@api_audit/views.py`:
- Around line 20-30: _date_range currently returns naive datetimes which will
cause warnings/incorrect comparisons when Django is running with USE_TZ=True;
update _date_range to make the parsed start and end datetimes timezone-aware
using django.utils.timezone.make_aware (preserving the added day on end), e.g.,
call timezone.make_aware on the parsed start and end before returning (keep the
existing ValueError handling and return signature).

In `@api_instances/serializers.py`:
- Around line 218-219: The InstanceParamHistorySerializer defines instance_name
= serializers.CharField(source="instance.instance_name") but it's an output-only
dotted-source field; modify InstanceParamHistorySerializer to mark instance_name
as read-only by setting read_only=True on that field (e.g., instance_name =
serializers.CharField(source="instance.instance_name", read_only=True)) so DRF
won't treat it as writable during deserialization.

In `@api_instances/tests.py`:
- Around line 24-32: FakeDictionaryResult.to_dict currently returns empty dicts
when column_list is empty or when row lengths don't match columns; update
FakeDictionaryResult.to_dict to validate and fail loudly: if column_list is
empty raise ValueError (or if column_list has length 1 allow scalar rows by
normalizing to single-element tuples), and for each row assert that its length
equals len(column_list) (raising ValueError with a clear message if not).
Reference: class FakeDictionaryResult and its to_dict method—add these
checks/normalization so tests or callers get an explicit error instead of silent
empty dicts.

In `@api_instances/urls.py`:
- Around line 17-120: The routes lack name= kwargs which prevents reverse() and
named URL lookup; update each path(...) entry (e.g., the ones referencing
DataDictionaryInstanceList, DataDictionaryDatabaseList, DataDictionaryTableList,
DataDictionaryTableDetail, DataDictionaryExport,
InstanceOperationDatabaseListCreate, InstanceOperationDatabaseInstanceList,
InstanceOperationDatabaseDetail, InstanceOperationAccountListCreate,
InstanceOperationAccountInstanceList, InstanceOperationAccountMetadata,
InstanceOperationAccountPassword, InstanceOperationAccountLock,
InstanceOperationAccountDelete, InstanceOperationAccountGrant,
InstanceOperationParamList, InstanceOperationParamInstanceList,
InstanceOperationParamHistory, InstanceOperationParamEdit,
InstanceOperationDiagnosticInstanceList, InstanceOperationDiagnosticProcessList,
InstanceOperationDiagnosticKillPreview, InstanceOperationDiagnosticKill,
InstanceOperationDiagnosticTablespace, InstanceOperationDiagnosticTransactions,
InstanceOperationDiagnosticLocks) to include a unique name="..." kwarg; pick
consistent, descriptive names (e.g., "data_dictionary.instances.list",
"instance_operations.database.create" or similar) and ensure names are unique
across the module so reverse("name") can be used reliably.

In `@api_instances/views.py`:
- Around line 143-150: The ValidationError being re-raised in helpers like
_data_dictionary_instance should chain the original exception to satisfy Ruff
B904; change each except block (e.g., except Instance.DoesNotExist:) to capture
the exception (except Instance.DoesNotExist as exc:) and re-raise the
serializers.ValidationError(...) using "raise serializers.ValidationError(... )
from exc" (or "from None" if you intentionally want to suppress the original
traceback where the lower-level error message is already surfaced), and apply
the same pattern for the other except Exception handlers listed (lines
referenced in the review) to preserve causal chains and silence the linter.

In `@frontend/src/features/audit/pages/AuditPage.vue`:
- Around line 158-186: setView currently mutates
filters.action/status/syntaxType and calls loadAuditLogs(), but those same
filter fields are watched by the watcher that triggers debouncedLoadAuditLogs(),
causing a duplicate fetch; to fix, remove the explicit loadAuditLogs() call from
setView and rely on the watcher (or alternatively cancel/suppress the debounce
when switching views), i.e. update the setView function (referencing setView,
filters, loadAuditLogs, debouncedLoadAuditLogs, and the watch callback) so only
one request is scheduled when switching views.

In `@frontend/src/features/instance-operations/manifest.ts`:
- Around line 17-50: The navigation items in the manifest reuse the same Wrench
icon making items hard to distinguish; update the navigation array so each route
uses its page-specific lucide icon: use Database for
'/instance-operations/databases', UserRound or KeyRound for
'/instance-operations/accounts' (pick the one matching the account page import),
SlidersHorizontal for '/instance-operations/parameters', and SquareActivity for
'/instance-operations/diagnostics' instead of Wrench; ensure the corresponding
icon identifiers (Database, UserRound/KeyRound, SlidersHorizontal,
SquareActivity) are imported at the top of the file and referenced in the
navigation entries.

In `@frontend/src/features/instance-operations/pages/AccountManagementPage.vue`:
- Around line 237-240: refreshAccounts currently calls loadInstances and
loadAccounts in parallel which causes a redundant no-op because loadAccounts
early-returns when selectedInstanceId is null and the [selectedInstanceId,
savedOnly] watcher then calls loadAccounts again after loadInstances sets the
id; fix by making refreshAccounts call await loadInstances() first, capture the
prior selectedInstanceId before the call, and only call loadAccounts()
afterwards if selectedInstanceId is non-null and equal to the prior value (i.e.,
the instance id did not change), so the watcher handles the case where
loadInstances sets a new id.
- Around line 518-705: The grid's xl columns are fixed causing wasted space when
no form is open; change the container div's static class
"xl:grid-cols-[minmax(0,1fr)_minmax(21rem,0.34fr)]" to a dynamic class bound to
a computed or method (e.g. computed name gridColsClass or getGridColsClass) that
returns 'xl:grid-cols-[minmax(0,1fr)_minmax(21rem,0.34fr)]' when activeFormMode
is truthy and 'xl:grid-cols-1' (or equivalent single-column class) when falsy,
then update the div to use :class="['grid gap-6', gridColsClass]" so the
right-hand Card collapses away when no form is open (refer to the container div,
activeFormMode, and the new computed gridColsClass).

In `@frontend/src/features/instance-operations/pages/DatabaseManagementPage.vue`:
- Around line 247-251: The Refresh button only checks loadingDatabases but
refreshDatabases() awaits both loadInstances() and loadDatabases(), so update
the button disabling logic to prevent clicks while either phase is running by
using both flags (e.g., change the :disabled binding on the Refresh Button to
combine loadingDatabases and loadingInstances), and ensure the component
exposes/sets loadingInstances around the loadInstances() call so the combined
check is reliable.

In `@frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue`:
- Around line 166-224: The initial mount triggers duplicate calls because
refreshAll() runs loadInstances(), loadParams(), and loadHistory() in parallel
while selectedInstanceId is still null, then the watcher on [selectedInstanceId,
editableFilter] fires when loadInstances sets selectedInstanceId; to fix, change
refreshAll() to await loadInstances() first and only then call loadParams() and
loadHistory() (e.g., await loadInstances(); await Promise.all([loadParams(),
loadHistory()])) so the watcher handles subsequent loads, or alternatively
conditionally skip calling loadParams()/loadHistory() in refreshAll() when
selectedInstanceId.value is falsy; update the refreshAll function (and keep the
watcher and onMounted flow) accordingly to eliminate the redundant early no-op
calls.

In `@frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue`:
- Around line 102-105: The current columns computed uses
Object.keys(firstRow).slice(0, 10) which silently drops columns; change it to
keep the full key list (const columns = computed(() => { const firstRow =
rows.value[0]; return firstRow ? Object.keys(firstRow) : [] })) or, if you must
limit for UI, create two computeds: displayColumns (first 10) and
extraColumnsCount (computed from Object.keys(firstRow).length -
displayColumns.length) and update the template to render displayColumns plus a
"+{extraColumnsCount} more columns" indicator; update references to columns in
the component accordingly (look for the columns computed and any template
usages).
- Around line 115-129: processId(row) is being called multiple times and its
truthiness treats a valid numeric id of 0 as "no id"; compute the id once and
use explicit null checks instead of truthy checks. Update the code paths that
render/disable/compare (calls around processId(row), toggleThread, and any
template bindings using processId) to call processId(row) a single time into a
const (e.g., const id = processId(row)), reuse that id in selectedThreadIds
checks and in :disabled, and change conditions to id !== null (or id == null)
rather than relying on truthiness so an id of 0 is handled correctly; also
ensure toggleThread already uses the single computed id and does not re-call
processId.

In `@frontend/src/features/inventory/pages/DataDictionaryPage.vue`:
- Around line 261-269: The synchronous call to
window.URL.revokeObjectURL(objectUrl) may race with the download start after
anchor.click(); defer revocation to ensure the browser has time to begin the
download by replacing the immediate revoke with a microtask or macrotask (e.g.,
queueMicrotask or setTimeout(..., 0)) so revokeObjectURL(objectUrl) runs shortly
after click; update the block that creates objectUrl/anchor (the objectUrl
variable and anchor.click() usage in DataDictionaryPage.vue) to revoke the URL
asynchronously instead of immediately.

In `@frontend/src/lib/api.ts`:
- Around line 1457-1475: The guard in fetchWorkflowOperationAuditLogs currently
uses truthy checks (if (options.audit_id), if (options.workflow_id), if
(options.workflow_type)) which will skip legitimate zero values; update each
guard to check for presence rather than truthiness (e.g., use options.audit_id
!= null or options.audit_id !== undefined, and the same for workflow_id and
workflow_type) so 0 is included in the URLSearchParams when provided; keep the
rest of the function (building params and calling apiGet/extractData) unchanged.

In `@sql/migrations/0015_alter_permission_options.py`:
- Line 95: The data migration currently uses
migrations.RunPython(remove_retired_sql_permissions, migrations.RunPython.noop)
which leaves deleted auth.Permission rows unrecoverable on rollback; replace the
noop with a proper reverse function (e.g., a recreate_retired_sql_permissions
undo) that recreates the removed Permission rows (using the same codename,
content_type and name used by remove_retired_sql_permissions) or explicitly
document/confirm that rollbacks must not rely on restoring those rows; update
the RunPython call to migrations.RunPython(remove_retired_sql_permissions,
recreate_retired_sql_permissions) so rollbacks return the DB to the
pre-migration state.

In `@sql/tests.py`:
- Line 190: Remove unnecessary f-string prefixes from literal URLs in the tests:
replace calls like self.client.get(f"/database/", data=data) with
self.client.get("/database/", data=data) (and similarly for the other
occurrences flagged by Ruff). Locate the self.client.get calls in sql/tests.py
that use f-strings without any interpolation and convert them to normal string
literals so Ruff F541 is resolved.

In `@sql/views.py`:
- Around line 51-52: The spa_redirect helper is redundant—remove the
spa_redirect function and replace its usages (calls to spa_redirect(...)) with
direct returns of HttpResponseRedirect(path); search for the spa_redirect symbol
and update call sites to use HttpResponseRedirect(path) so behavior remains
identical, then delete the spa_redirect definition from the module to avoid dead
code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 356589c9-69b2-4149-88fb-34edac17c76d

📥 Commits

Reviewing files that changed from the base of the PR and between 7b098b5 and 1c85666.

📒 Files selected for processing (91)
  • README.md
  • api_admin/settings.py
  • api_audit/__init__.py
  • api_audit/serializers.py
  • api_audit/tests.py
  • api_audit/urls.py
  • api_audit/views.py
  • api_instances/serializers.py
  • api_instances/tests.py
  • api_instances/urls.py
  • api_instances/views.py
  • common/templates/base.html
  • common/templates/config.html
  • docs/spa-bootstrap-parity-checklist.md
  • frontend/src/app/feature-registry.ts
  • frontend/src/features/archives/pages/ArchiveCreatePage.vue
  • frontend/src/features/archives/pages/ArchiveDetailPage.vue
  • frontend/src/features/archives/pages/ArchivesPage.vue
  • frontend/src/features/audit/api.ts
  • frontend/src/features/audit/manifest.ts
  • frontend/src/features/audit/pages/AuditPage.vue
  • frontend/src/features/instance-operations/api.ts
  • frontend/src/features/instance-operations/manifest.ts
  • frontend/src/features/instance-operations/pages/AccountManagementPage.vue
  • frontend/src/features/instance-operations/pages/DatabaseManagementPage.vue
  • frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue
  • frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue
  • frontend/src/features/inventory/api.ts
  • frontend/src/features/inventory/manifest.ts
  • frontend/src/features/inventory/pages/DataDictionaryPage.vue
  • frontend/src/features/inventory/pages/InventoryEditorPage.vue
  • frontend/src/features/inventory/pages/InventoryListPage.vue
  • frontend/src/features/permissions/pages/PermissionManagementPage.vue
  • frontend/src/features/queries/pages/QueriesPage.vue
  • frontend/src/features/settings/pages/SettingsGroupDetailPage.vue
  • frontend/src/features/settings/pages/SettingsGroupsPage.vue
  • frontend/src/features/settings/pages/SettingsInstanceTagDetailPage.vue
  • frontend/src/features/settings/pages/SettingsInstanceTagsPage.vue
  • frontend/src/features/settings/pages/SettingsResourceGroupDetailPage.vue
  • frontend/src/features/settings/pages/SettingsResourceGroupsPage.vue
  • frontend/src/features/settings/system-settings.ts
  • frontend/src/features/workflows/pages/WorkflowDetailPage.vue
  • frontend/src/features/workflows/pages/WorkflowSqlEditorPage.vue
  • frontend/src/features/workflows/pages/WorkflowsPage.vue
  • frontend/src/lib/api.ts
  • frontend/tests/e2e/spa-parity.spec.ts
  • requirements.txt
  • sql/binlog.py
  • sql/fixtures/auth_group.sql
  • sql/instance.py
  • sql/local_demo.py
  • sql/migrations/0015_alter_permission_options.py
  • sql/models.py
  • sql/notify.py
  • sql/plugins/my2sql.py
  • sql/plugins/schemasync.py
  • sql/plugins/soar.py
  • sql/plugins/sqladvisor.py
  • sql/plugins/tests.py
  • sql/slowlog.py
  • sql/sql_analyze.py
  • sql/sql_optimize.py
  • sql/templates/audit.html
  • sql/templates/audit_sqlquery.html
  • sql/templates/audit_sqlworkflow.html
  • sql/templates/data_dictionary.html
  • sql/templates/database.html
  • sql/templates/dbaprinciples.html
  • sql/templates/dbdiagnostic.html
  • sql/templates/instanceaccount.html
  • sql/templates/my2sql.html
  • sql/templates/param.html
  • sql/templates/schemasync.html
  • sql/templates/slowquery.html
  • sql/templates/sqladvisor.html
  • sql/templates/sqlanalyze.html
  • sql/test_notify.py
  • sql/tests.py
  • sql/urls.py
  • sql/utils/sql_utils.py
  • sql/views.py
  • sql_api/urls.py
  • src/charts/templates/configMap.yaml
  • src/charts/values.yaml
  • src/docker-compose/datamingle/soar.yaml
  • src/docker-compose/docker-compose.local-arm.yml
  • src/docker-compose/docker-compose.yml
  • src/docker/Dockerfile
  • src/docker/Dockerfile-base
  • src/docker/setup.sh
  • src/plugins/soar.yaml
💤 Files with no reviewable changes (38)
  • requirements.txt
  • src/docker-compose/docker-compose.yml
  • src/docker-compose/docker-compose.local-arm.yml
  • src/docker/Dockerfile
  • src/plugins/soar.yaml
  • common/templates/config.html
  • src/docker/Dockerfile-base
  • sql/templates/audit.html
  • sql/templates/audit_sqlworkflow.html
  • sql/plugins/schemasync.py
  • README.md
  • sql/plugins/sqladvisor.py
  • sql/templates/dbaprinciples.html
  • sql/plugins/my2sql.py
  • api_admin/settings.py
  • src/docker/setup.sh
  • sql/plugins/soar.py
  • sql/templates/dbdiagnostic.html
  • src/charts/values.yaml
  • sql/sql_analyze.py
  • sql/templates/param.html
  • sql/templates/audit_sqlquery.html
  • sql/templates/my2sql.html
  • sql/test_notify.py
  • sql/templates/sqladvisor.html
  • sql/binlog.py
  • src/docker-compose/datamingle/soar.yaml
  • sql/sql_optimize.py
  • sql/instance.py
  • sql/templates/schemasync.html
  • sql/templates/data_dictionary.html
  • sql/slowlog.py
  • sql/templates/sqlanalyze.html
  • sql/models.py
  • sql/templates/slowquery.html
  • sql/local_demo.py
  • sql/urls.py
  • sql/templates/database.html

Comment thread api_audit/tests.py
Comment thread api_audit/views.py
Comment thread api_instances/serializers.py Outdated
Comment thread api_instances/views.py
Comment thread api_instances/views.py
Comment thread frontend/src/features/inventory/pages/DataDictionaryPage.vue
Comment on lines +5 to +24
RETIRED_SQL_PERMISSION_CODENAMES = (
"menu_sqlanalyze",
"menu_sqloptimize",
"menu_sqladvisor",
"menu_slowquery",
"menu_my2sql",
"menu_schemasync",
"menu_document",
"sql_analyze",
"optimize_sqladvisor",
"optimize_sqltuning",
"optimize_soar",
)


def remove_retired_sql_permissions(apps, schema_editor):
Permission = apps.get_model("auth", "Permission")
Permission.objects.filter(
content_type__app_label="sql", codename__in=RETIRED_SQL_PERMISSION_CODENAMES
).delete()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for any remaining references to retired permission codenames across the repo.
codenames=(
  menu_sqlanalyze menu_sqloptimize menu_sqladvisor menu_slowquery
  menu_my2sql menu_schemasync menu_document
  sql_analyze optimize_sqladvisor optimize_sqltuning optimize_soar
)
for cn in "${codenames[@]}"; do
  echo "=== $cn ==="
  rg -nP "\b${cn}\b" -g '!**/migrations/**' -g '!**/*.pyc'
done

Repository: jruszo/Datamingle

Length of output: 2362


Coordinate seed data with migration: seed SQL files reinsert deleted permissions.

Seed SQL files (src/init_sql/v*.sql) contain INSERT statements for the exact codenames this migration deletes:

  • menu_sqlanalyze, menu_sqloptimize, menu_sqladvisor, menu_slowquery (v1.2.0.sql)
  • menu_my2sql (v1.8.3.sql)
  • menu_schemasync (v1.3.2.sql)
  • menu_document, sql_analyze, optimize_sqladvisor, optimize_sqltuning (v1.2.0.sql, v1.4.5.sql)

If seed data runs before migrations, these permissions are inserted then immediately deleted. Remove these INSERT statements from the seed SQL files, or clarify the intended initialization order.

Test and documentation references to these codenames (e.g., /sql_analyze/ endpoints in api_instances/tests.py) are URL paths, not permission checks, so they are not affected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/migrations/0015_alter_permission_options.py` around lines 5 - 24, The
migration remove_retired_sql_permissions deletes permissions listed in
RETIRED_SQL_PERMISSION_CODENAMES but seed SQL files still INSERT those same
codenames, causing them to be re-created then removed; fix by updating the seed
SQL files (src/init_sql/v*.sql) to remove or conditionally skip INSERTs for the
listed codenames (menu_sqlanalyze, menu_sqloptimize, menu_sqladvisor,
menu_slowquery, menu_my2sql, menu_schemasync, menu_document, sql_analyze,
optimize_sqladvisor, optimize_sqltuning, optimize_soar), or change
initialization ordering so seeds run after this migration, and add a short note
in docs/tests indicating these codenames are retired (referencing
RETIRED_SQL_PERMISSION_CODENAMES and remove_retired_sql_permissions to locate
the migration).

Comment thread sql/plugins/tests.py Outdated
Comment thread sql/views.py Outdated
Comment on lines +633 to +648
@permission_required("sql.audit_user", raise_exception=True)
def audit(request):
"""General audit log page."""
_action_types = AuditEntry.objects.values_list("action").distinct()
action_types = [i[0] for i in _action_types]
return render(request, "audit.html", {"action_types": action_types})
return spa_redirect("/audit")


@permission_required("sql.audit_user", raise_exception=True)
def audit_sqlquery(request):
"""Online SQL query audit page."""
user = request.user
favorites = QueryLog.objects.filter(username=user.username, favorite=True).values(
"id", "alias"
)
return render(request, "audit_sqlquery.html", {"favorites": favorites})
return spa_redirect("/audit")


@permission_required("sql.audit_user", raise_exception=True)
def audit_sqlworkflow(request):
"""SQL review workflow list page."""
user = request.user
# Data for filter options
filter_dict = dict()
# Admin users can view all workflows
if user.is_superuser or user.has_perm("sql.audit_user"):
pass
# Non-admin users with review or resource-group execution permission
# can view all workflows in their groups.
elif user.has_perm("sql.sql_review") or user.has_perm(
"sql.sql_execute_for_resource_group"
):
# Get the user's resource groups first
group_list = user_groups(user)
group_ids = [group.group_id for group in group_list]
filter_dict["group_id__in"] = group_ids
# Everyone else can only view their own workflows
else:
filter_dict["engineer"] = user.username
instance_id = (
SqlWorkflow.objects.filter(**filter_dict).values("instance_id").distinct()
)
instance = Instance.objects.filter(pk__in=instance_id)
resource_group_id = (
SqlWorkflow.objects.filter(**filter_dict).values("group_id").distinct()
)
resource_group = ResourceGroup.objects.filter(group_id__in=resource_group_id)

return render(
request,
"audit_sqlworkflow.html",
{
"status_list": SQL_WORKFLOW_CHOICES,
"instance": instance,
"resource_group": resource_group,
},
)
return spa_redirect("/audit")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm AuditPage.vue handles a query param for tab/category selection.
fd -t f 'AuditPage.vue' frontend
fd -t f 'AuditPage.vue' frontend --exec rg -nP -C3 '\b(useRoute|route\.query|tab)\b' {}

Repository: jruszo/Datamingle

Length of output: 109


🏁 Script executed:

cat frontend/src/features/audit/pages/AuditPage.vue

Repository: jruszo/Datamingle

Length of output: 14186


🏁 Script executed:

rg -n 'def spa_redirect' sql/

Repository: jruszo/Datamingle

Length of output: 101


🏁 Script executed:

sed -n '51,65p' sql/views.py

Repository: jruszo/Datamingle

Length of output: 441


Three audit endpoints collapse to the same /audit URL — bookmarks/deep links lose context.

audit, audit_sqlquery, and audit_sqlworkflow all redirect to /audit, losing the distinction between general, query, and workflow audit pages. An existing bookmark to the SQL-workflow audit will land on the general tab instead of the intended category.

The suggested approach of passing tab parameters (e.g., /audit?tab=general, /audit?tab=query, /audit?tab=sql-workflow) is valid but requires updates to AuditPage.vue. Currently, the component initializes activeView to 'general' on mount and does not read query parameters from the route. Add query parameter handling to onMounted() to read and select the correct tab before loading data.

♻️ Suggested changes

Backend (sql/views.py):

 `@permission_required`("sql.audit_user", raise_exception=True)
 def audit(request):
     """General audit log page."""
-    return spa_redirect("/audit")
+    return spa_redirect("/audit?tab=general")


 `@permission_required`("sql.audit_user", raise_exception=True)
 def audit_sqlquery(request):
     """Online SQL query audit page."""
-    return spa_redirect("/audit")
+    return spa_redirect("/audit?tab=query")


 `@permission_required`("sql.audit_user", raise_exception=True)
 def audit_sqlworkflow(request):
     """SQL review workflow list page."""
-    return spa_redirect("/audit")
+    return spa_redirect("/audit?tab=sql-workflow")

Frontend (frontend/src/features/audit/pages/AuditPage.vue):

 onMounted(async () => {
   await authStore.loadCurrentUser()
+  const route = useRoute()
+  const tabParam = route.query.tab
+  if (tabParam === 'query') activeView.value = 'query'
+  else if (tabParam === 'sql-workflow') activeView.value = 'workflow'
   await loadAuditLogs()
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/views.py` around lines 633 - 648, The three view functions audit,
audit_sqlquery, and audit_sqlworkflow currently all redirect to "/audit", which
loses tab context; update each to append a tab query parameter (e.g.,
"/audit?tab=general", "/audit?tab=query", "/audit?tab=sql-workflow") so
bookmarks retain intent, and then update the frontend AuditPage.vue to read the
route query in its onMounted() hook: have onMounted() inspect route.query.tab
(falling back to 'general'), set activeView accordingly, and only then trigger
data loading so the correct tab content is displayed on initial load.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (1)
frontend/src/features/instance-operations/pages/AccountManagementPage.vue (1)

441-449: ⚠️ Potential issue | 🟡 Minor

Handle loadCurrentUser() failures before checking permissions.

If the session refresh rejects, this page exits through an unhandled promise rejection and never surfaces a user-facing error. DatabaseManagementPage.vue already wraps the same call in toUserFacingMessage; this page needs the same guard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/AccountManagementPage.vue`
around lines 441 - 449, The onMounted hook calls authStore.loadCurrentUser()
without handling rejections, causing unhandled promise rejections before the
permission check; wrap the call in toUserFacingMessage (like
DatabaseManagementPage.vue) and set error.value with the returned user-facing
message if it fails, then return early before checking canViewAccounts and
before calling refreshAccounts; modify the onMounted block around
authStore.loadCurrentUser(), canViewAccounts, error.value, and refreshAccounts
to use this guarded pattern.
🧹 Nitpick comments (5)
sql/plugins/tests.py (1)

107-114: Strengthen test_plugin_generate_args2cmd assertion.

assertIsInstance(cmd_args, list) only checks the return type and would pass even if generate_args2cmd returned [] or had a regression in argument formatting. Consider asserting the actual command-arg structure produced by Plugin.generate_args2cmd.

♻️ Proposed refinement
     def test_plugin_generate_args2cmd(self):
         """
         Test argument conversion.
         :return:
         """
         plugin = Plugin(path="/usr/bin/example")
         cmd_args = plugin.generate_args2cmd({"query": "select 1;"})
-        self.assertIsInstance(cmd_args, list)
+        self.assertEqual(cmd_args, ["/usr/bin/example", "-query", "select 1;"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/plugins/tests.py` around lines 107 - 114, The test
test_plugin_generate_args2cmd is too weak—replace the lone assertIsInstance with
concrete assertions that validate the produced command argument structure from
Plugin.generate_args2cmd; call
Plugin(path="/usr/bin/example").generate_args2cmd({"query":"select 1;"}) and
assert the list is non-empty, that it includes the query string ("select 1;") in
one of the elements, and that the command/binary (plugin.path or
"/usr/bin/example") appears in the args (or assert equality against the exact
expected list if the expected format is known) so regressions in formatting or
missing args are caught.
frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue (1)

222-228: Watcher races with refreshAll on first instance selection.

refreshAll() (called in onMounted) calls loadInstances(), which sets selectedInstanceId.value to the first instance when one is loaded. That triggers this watch callback synchronously and kicks off loadParams()/loadHistory() concurrently with the awaited Promise.all([loadParams(), loadHistory()]) already running inside refreshAll (well, inside if block guarded by priorInstanceId). On a fresh mount priorInstanceId === null, so refreshAll skips the inner Promise.all, but the watcher will still fire and load. That part works out, but it's fragile — any future change to refreshAll's guard could double-load.

Consider gating the watcher on a refreshing-style flag (as DataDictionaryPage.vue does) to make the intent explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue`
around lines 222 - 228, The watcher on [selectedInstanceId, editableFilter]
races with refreshAll; add a reactive flag (e.g., refreshing or isRefreshing)
used by refreshAll() and the watcher: set refreshing.value = true at the start
of refreshAll() and clear it in a finally block after its await-ed Promise.all
finishes, then update the watcher (the callback that currently calls closeEdit()
and void Promise.all([loadParams(), loadHistory()])) to early-return when
refreshing.value is true so it won’t trigger concurrent loads; keep calls to
closeEdit() only when not refreshing and keep using loadParams() and
loadHistory() as before.
frontend/src/features/audit/pages/AuditPage.vue (1)

208-215: Tab is parsed only on mount; SPA navigation between ?tab= values won't re-sync.

route.query.tab is read inside onMounted once. If a user navigates to /audit?tab=query while already on /audit?tab=general via <RouterLink> or programmatic push, the component is reused (same route name) and activeView won't update. If that flow exists (e.g., sidebar nav or deep links cross-tab), consider a watch(() => route.query.tab, ...) to keep them in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/audit/pages/AuditPage.vue` around lines 208 - 215, The
current logic that sets activeView (in the AuditPage.vue setup, where route and
activeView are defined and currently initialized onMounted) only runs once and
won't react to SPA navigation; add a watcher on route.query.tab (e.g., watch(()
=> route.query.tab, ...)) that applies the same parsing logic (use
Array.isArray(route.query.tab) ? route.query.tab[0] : route.query.tab and map
'query' -> activeView.value='query', 'workflow' or 'sql-workflow' -> 'workflow',
otherwise 'general') so activeView updates whenever the query param changes;
keep the existing onMounted initialization but move the shared parsing logic
into a small helper or reuse it from the watcher to avoid duplication.
sql/views.py (1)

463-490: Optional: consider hardcoded path reuse and query-string preservation.

The five redirects are straightforward and correctly preserve the permission_required decorators. Two minor observations you may want to consider:

  1. The SPA target paths are duplicated as string literals here and again in frontend/src/features/*/manifest.ts. If these legacy entry points stay long-term, a small constant module (e.g., SPA_ROUTES) could prevent drift.
  2. HttpResponseRedirect drops the original request's query string. If any external link/bookmark passes filters (e.g., /database/?instance=foo), they're silently lost. If that's intentional for these legacy entries, ignore.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/views.py` around lines 463 - 490, The redirect handlers instanceaccount,
database, dbdiagnostic, data_dictionary, and instance_param duplicate hardcoded
SPA target strings and drop any incoming query string; refactor by extracting
those target paths into a shared constant (e.g., SPA_ROUTES with keys for
accounts, databases, diagnostics, data_dictionary, parameters) and update each
view to read the appropriate SPA_ROUTES entry, then preserve the original
request query string when redirecting (append request.META.get('QUERY_STRING')
when non-empty) before returning HttpResponseRedirect so query parameters like
?instance=... are not lost.
frontend/src/features/inventory/pages/DataDictionaryPage.vue (1)

98-113: normalizeRows fallback for non-array rows is suspect.

When rows is a non-array, non-null primitive, the function returns [[rows]] — a 1×1 row matrix wrapping a scalar. This is unlikely to be useful and will silently render that scalar in the column-zero cell of a single row, regardless of column count. If a backend ever sends a malformed payload, you'll see a one-row table with a cryptic value rather than an explicit error. Consider returning [] (and surfacing a console warning) for that branch instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/inventory/pages/DataDictionaryPage.vue` around lines 98
- 113, The normalizeRows function currently turns a non-array, non-null
primitive rows into [[rows]] which can silently render invalid data; change
normalizeRows so that when rows is neither an array nor null/undefined it
returns [] and logs a console.warn (or use the existing logger) describing the
unexpected rows payload; keep existing behavior for Array and empty array
branches and ensure you update the branch that now returns [[rows]] (inside
normalizeRows) to instead warn and return [].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api_audit/views.py`:
- Around line 203-223: The lookup passes raw strings to
WorkflowAudit.objects.get which can raise a 500 if workflow_id or workflow_type
are non-numeric; validate/convert both workflow_id and workflow_type before
calling WorkflowAudit.objects.get (similar to the audit_id handling): attempt to
cast workflow_id and workflow_type to int, raise serializers.ValidationError
with clear messages on ValueError, and only call
WorkflowAudit.objects.get(workflow_id=workflow_id, workflow_type=workflow_type)
after successful conversions.

In `@api_instances/serializers.py`:
- Around line 152-162: InstanceAccountPasswordSerializer currently accepts
whitespace-only passwords while InstanceAccountPayloadSerializer blocks them;
update InstanceAccountPasswordSerializer.password to use the same non-blank
validator used by InstanceAccountPayloadSerializer (e.g. add
validators=[validate_not_blank] or the project’s equivalent validator) so that
InstanceOperationAccountPassword.post cannot accept whitespace-only reset
passwords—modify the password field declaration in
InstanceAccountPasswordSerializer to include that validator.

In `@api_instances/views.py`:
- Around line 345-360: _account_metadata_fields currently only extracts host
from the quoted form "`user`@`host`", causing missing host when callers pass raw
"user@host"; update the MySQL branch in _account_metadata_fields to handle both
forms: read user_host = data.get("user_host",""), if user_host contains "@`"
keep existing rsplit logic, else if user_host contains "@" split on the last "@"
(user_host.rsplit("@",1)), strip any surrounding backticks or quotes from both
parts, and assign host (and user if missing) so metadata lookups match
_mysql_user_host and avoid duplicate/stale rows.
- Around line 1138-1145: The code uses InstanceDatabase.objects.create(...)
after creating the live DB which can raise IntegrityError if a metadata-only row
already exists; replace the create() call with an idempotent upsert such as
InstanceDatabase.objects.update_or_create((instance=instance, db_name=db_name),
defaults={'owner': owner, 'owner_display': owner_display, 'remark': remark}) or
equivalent get-or-create + update flow to avoid IntegrityError and ensure
existing metadata is updated; keep the subsequent
_clear_instance_resource_cache() call. Reference InstanceDatabase,
InstanceOperationDatabaseDetail.put, create(), and
_clear_instance_resource_cache().

In `@frontend/src/features/audit/pages/AuditPage.vue`:
- Around line 168-180: The pagination can be overwritten by a pending debounced
filter load; modify movePage to cancel or flush the debounced call before
changing page so a pending debounced handler doesn't reset filters.page to 1.
Specifically, call the cancel/flush method returned by useDebounceFn (refer to
debouncedLoadAuditLogs) or use the rejectOnCancel option and invoke
debouncedLoadAuditLogs.cancel()/flush() prior to setting filters.page and
calling loadAuditLogs(), ensuring loadAuditLogs() is the authoritative immediate
fetch after pagination.

In `@frontend/src/features/instance-operations/pages/AccountManagementPage.vue`:
- Around line 250-263: accountPayload() currently returns snake_case keys
instance_id and user but updateInstanceOperationAccount expects
payload.instanceId and payload.username when building the edit URL; change
accountPayload() (and the similar payload at lines 286-297) to return instanceId
(from selectedInstanceId.value) and username (from accountForm.user.trim())
along with the other fields (dbName, host, password, remark) so the API function
receives the correctly shaped payload and the edit URL is built without
undefined segments.

In `@frontend/src/features/instance-operations/pages/DatabaseManagementPage.vue`:
- Around line 157-162: The list call is using the wrong argument order and
payload keys for the frontend API helper: fetchInstanceOperationDatabases
expects (instanceId, token) and the API payloads use camelCase fields
instanceId/dbName, but the page passes (token, { instance_id, db_name, saved })
which makes the token treated as the instanceId and leaves path params undefined
for edits. Fix by calling fetchInstanceOperationDatabases with
selectedInstanceId.value as the first arg and requireToken() as the second, and
update any edit/patch payloads (the create/edit calls on this page) to use
camelCase keys instanceId and dbName (and pass the instanceId value from
selectedInstanceId.value) so the helper can build the correct URLs.

In `@frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue`:
- Around line 207-220: The onMounted block awaits authStore.loadCurrentUser()
without error handling which can leave the page blank on network/token errors;
wrap the await in a try/catch, convert the caught error via toUserFacingMessage
and set error.value accordingly (then return) before proceeding to permission
checks and refreshAll; update the onMounted handler that calls
authStore.loadCurrentUser(), uses toUserFacingMessage, and ensures refreshAll is
only called on success.

In `@frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue`:
- Around line 170-180: The diagnostic fetch calls are passing arguments in the
wrong order causing the token to be used as payload; update the calls where
activeTab === 'processes' and 'tablespace' to call
fetchInstanceOperationDiagnosticProcesses(payload, token) and
fetchInstanceOperationDiagnosticTablespace(payload, token) respectively (build
the payload using selectedInstanceId.value, commandType.value or size as before)
and pass requireToken() as the second argument so the payload (object) is the
first parameter and the auth token is the second; adjust the two call sites in
SessionDiagnosticsPage.vue accordingly.

---

Duplicate comments:
In `@frontend/src/features/instance-operations/pages/AccountManagementPage.vue`:
- Around line 441-449: The onMounted hook calls authStore.loadCurrentUser()
without handling rejections, causing unhandled promise rejections before the
permission check; wrap the call in toUserFacingMessage (like
DatabaseManagementPage.vue) and set error.value with the returned user-facing
message if it fails, then return early before checking canViewAccounts and
before calling refreshAccounts; modify the onMounted block around
authStore.loadCurrentUser(), canViewAccounts, error.value, and refreshAccounts
to use this guarded pattern.

---

Nitpick comments:
In `@frontend/src/features/audit/pages/AuditPage.vue`:
- Around line 208-215: The current logic that sets activeView (in the
AuditPage.vue setup, where route and activeView are defined and currently
initialized onMounted) only runs once and won't react to SPA navigation; add a
watcher on route.query.tab (e.g., watch(() => route.query.tab, ...)) that
applies the same parsing logic (use Array.isArray(route.query.tab) ?
route.query.tab[0] : route.query.tab and map 'query' ->
activeView.value='query', 'workflow' or 'sql-workflow' -> 'workflow', otherwise
'general') so activeView updates whenever the query param changes; keep the
existing onMounted initialization but move the shared parsing logic into a small
helper or reuse it from the watcher to avoid duplication.

In `@frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue`:
- Around line 222-228: The watcher on [selectedInstanceId, editableFilter] races
with refreshAll; add a reactive flag (e.g., refreshing or isRefreshing) used by
refreshAll() and the watcher: set refreshing.value = true at the start of
refreshAll() and clear it in a finally block after its await-ed Promise.all
finishes, then update the watcher (the callback that currently calls closeEdit()
and void Promise.all([loadParams(), loadHistory()])) to early-return when
refreshing.value is true so it won’t trigger concurrent loads; keep calls to
closeEdit() only when not refreshing and keep using loadParams() and
loadHistory() as before.

In `@frontend/src/features/inventory/pages/DataDictionaryPage.vue`:
- Around line 98-113: The normalizeRows function currently turns a non-array,
non-null primitive rows into [[rows]] which can silently render invalid data;
change normalizeRows so that when rows is neither an array nor null/undefined it
returns [] and logs a console.warn (or use the existing logger) describing the
unexpected rows payload; keep existing behavior for Array and empty array
branches and ensure you update the branch that now returns [[rows]] (inside
normalizeRows) to instead warn and return [].

In `@sql/plugins/tests.py`:
- Around line 107-114: The test test_plugin_generate_args2cmd is too
weak—replace the lone assertIsInstance with concrete assertions that validate
the produced command argument structure from Plugin.generate_args2cmd; call
Plugin(path="/usr/bin/example").generate_args2cmd({"query":"select 1;"}) and
assert the list is non-empty, that it includes the query string ("select 1;") in
one of the elements, and that the command/binary (plugin.path or
"/usr/bin/example") appears in the args (or assert equality against the exact
expected list if the expected format is known) so regressions in formatting or
missing args are caught.

In `@sql/views.py`:
- Around line 463-490: The redirect handlers instanceaccount, database,
dbdiagnostic, data_dictionary, and instance_param duplicate hardcoded SPA target
strings and drop any incoming query string; refactor by extracting those target
paths into a shared constant (e.g., SPA_ROUTES with keys for accounts,
databases, diagnostics, data_dictionary, parameters) and update each view to
read the appropriate SPA_ROUTES entry, then preserve the original request query
string when redirecting (append request.META.get('QUERY_STRING') when non-empty)
before returning HttpResponseRedirect so query parameters like ?instance=... are
not lost.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e44f2d4a-ed7f-4cf9-b957-9d5b0227f9ee

📥 Commits

Reviewing files that changed from the base of the PR and between 1c85666 and 147f827.

📒 Files selected for processing (67)
  • README.md
  • admin.sh
  • api_admin/dashboard.py
  • api_audit/tests.py
  • api_audit/urls.py
  • api_audit/views.py
  • api_instances/serializers.py
  • api_instances/tests.py
  • api_instances/urls.py
  • api_instances/views.py
  • common/dashboard.py
  • common/templates/dashboard.html
  • common/utils/chart_dao.py
  • docs/spa-bootstrap-parity-checklist.md
  • frontend/src/features/audit/pages/AuditPage.vue
  • frontend/src/features/dashboard/pages/HomePage.vue
  • frontend/src/features/instance-operations/manifest.ts
  • frontend/src/features/instance-operations/pages/AccountManagementPage.vue
  • frontend/src/features/instance-operations/pages/DatabaseManagementPage.vue
  • frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue
  • frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue
  • frontend/src/features/inventory/pages/DataDictionaryPage.vue
  • frontend/src/lib/api.ts
  • sql/migrations/0015_alter_permission_options.py
  • sql/plugins/tests.py
  • sql/tests.py
  • sql/views.py
  • src/charts/templates/configMap.yaml
  • src/charts/values.yaml
  • src/docker-compose/mysql/my.cnf
  • src/init_sql/del_permissions.sql
  • src/init_sql/goinception_param_template.sql
  • src/init_sql/mysql_slow_query_review.sql
  • src/init_sql/rds_param_template.sql
  • src/init_sql/v1.0_init.sql
  • src/init_sql/v1.1.0.sql
  • src/init_sql/v1.10.0.sql
  • src/init_sql/v1.12.0.sql
  • src/init_sql/v1.13.0.sql
  • src/init_sql/v1.2.0.sql
  • src/init_sql/v1.3.0.sql
  • src/init_sql/v1.3.2.sql
  • src/init_sql/v1.3.7.sql
  • src/init_sql/v1.4.0.sql
  • src/init_sql/v1.4.3.sql
  • src/init_sql/v1.4.5.sql
  • src/init_sql/v1.5.0.sql
  • src/init_sql/v1.5.3_comment.sql
  • src/init_sql/v1.6.0.sql
  • src/init_sql/v1.6.1.sql
  • src/init_sql/v1.6.2.sql
  • src/init_sql/v1.6.3.sql
  • src/init_sql/v1.6.6.sql
  • src/init_sql/v1.6.7.sql
  • src/init_sql/v1.7.0.sql
  • src/init_sql/v1.7.1.sql
  • src/init_sql/v1.7.11.sql
  • src/init_sql/v1.7.12.sql
  • src/init_sql/v1.7.2.sql
  • src/init_sql/v1.7.3.sql
  • src/init_sql/v1.7.5.sql
  • src/init_sql/v1.7.7.sql
  • src/init_sql/v1.7.8.sql
  • src/init_sql/v1.8.3.sql
  • src/init_sql/v1.8.4.sql
  • src/init_sql/v1.9.0.sql
  • src/script/analysis_slow_query.sh
💤 Files with no reviewable changes (25)
  • admin.sh
  • src/docker-compose/mysql/my.cnf
  • src/init_sql/v1.3.7.sql
  • src/init_sql/v1.3.2.sql
  • src/init_sql/v1.10.0.sql
  • src/init_sql/v1.3.0.sql
  • src/init_sql/v1.13.0.sql
  • src/init_sql/v1.4.0.sql
  • src/init_sql/v1.12.0.sql
  • src/init_sql/del_permissions.sql
  • api_admin/dashboard.py
  • src/init_sql/goinception_param_template.sql
  • src/init_sql/v1.4.3.sql
  • common/templates/dashboard.html
  • src/init_sql/rds_param_template.sql
  • src/init_sql/v1.1.0.sql
  • src/init_sql/v1.2.0.sql
  • src/charts/values.yaml
  • common/dashboard.py
  • src/init_sql/mysql_slow_query_review.sql
  • src/init_sql/v1.5.0.sql
  • frontend/src/features/dashboard/pages/HomePage.vue
  • src/init_sql/v1.0_init.sql
  • common/utils/chart_dao.py
  • src/init_sql/v1.4.5.sql
✅ Files skipped from review due to trivial changes (1)
  • docs/spa-bootstrap-parity-checklist.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • api_audit/urls.py
  • frontend/src/features/instance-operations/manifest.ts
  • src/charts/templates/configMap.yaml
  • README.md
  • frontend/src/lib/api.ts

Comment thread api_audit/views.py
Comment on lines +203 to +223
audit_id = request.query_params.get("audit_id", "").strip()
workflow_id = request.query_params.get("workflow_id", "").strip()
workflow_type = request.query_params.get("workflow_type", "").strip() or "2"

if not audit_id and not workflow_id:
raise serializers.ValidationError(
{"errors": "audit_id or workflow_id is required."}
)

if audit_id:
try:
audit_id = int(audit_id)
except ValueError as exc:
raise serializers.ValidationError(
{"audit_id": "A valid integer is required."}
) from exc
else:
try:
audit_id = WorkflowAudit.objects.get(
workflow_id=workflow_id, workflow_type=workflow_type
).audit_id
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate workflow_id and workflow_type before the lookup.

The new audit_id guard fixes one 500 path, but WorkflowAudit.objects.get(workflow_id=workflow_id, workflow_type=workflow_type) still receives raw strings. A non-numeric workflow_id or workflow_type will raise during queryset prep and bypass the API's 400 validation flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_audit/views.py` around lines 203 - 223, The lookup passes raw strings to
WorkflowAudit.objects.get which can raise a 500 if workflow_id or workflow_type
are non-numeric; validate/convert both workflow_id and workflow_type before
calling WorkflowAudit.objects.get (similar to the audit_id handling): attempt to
cast workflow_id and workflow_type to int, raise serializers.ValidationError
with clear messages on ValueError, and only call
WorkflowAudit.objects.get(workflow_id=workflow_id, workflow_type=workflow_type)
after successful conversions.

Comment on lines +152 to +162
class InstanceAccountPasswordSerializer(serializers.Serializer):
instance_id = serializers.IntegerField()
db_name = serializers.CharField(max_length=128, allow_blank=True, required=False)
db_name_user = serializers.CharField(
max_length=260, allow_blank=True, required=False
)
user_host = serializers.CharField(max_length=260, allow_blank=True, required=False)
user = serializers.CharField(max_length=128)
host = serializers.CharField(max_length=64, allow_blank=True, required=False)
password = serializers.CharField(max_length=128, write_only=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Apply the same non-blank password validation to reset requests.

InstanceAccountPayloadSerializer rejects whitespace-only passwords, but InstanceAccountPasswordSerializer doesn't. In InstanceOperationAccountPassword.post, a value like " " is truthy, so it bypasses the required-field check and can be accepted as a reset password.

Proposed fix
 class InstanceAccountPasswordSerializer(serializers.Serializer):
     instance_id = serializers.IntegerField()
     db_name = serializers.CharField(max_length=128, allow_blank=True, required=False)
     db_name_user = serializers.CharField(
         max_length=260, allow_blank=True, required=False
     )
     user_host = serializers.CharField(max_length=260, allow_blank=True, required=False)
     user = serializers.CharField(max_length=128)
     host = serializers.CharField(max_length=64, allow_blank=True, required=False)
     password = serializers.CharField(max_length=128, write_only=True)
+
+    def validate_password(self, value):
+        if not value.strip():
+            raise serializers.ValidationError("Password cannot be blank.")
+        return value
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/serializers.py` around lines 152 - 162,
InstanceAccountPasswordSerializer currently accepts whitespace-only passwords
while InstanceAccountPayloadSerializer blocks them; update
InstanceAccountPasswordSerializer.password to use the same non-blank validator
used by InstanceAccountPayloadSerializer (e.g. add
validators=[validate_not_blank] or the project’s equivalent validator) so that
InstanceOperationAccountPassword.post cannot accept whitespace-only reset
passwords—modify the password field declaration in
InstanceAccountPasswordSerializer to include that validator.

Comment thread api_instances/views.py
Comment on lines +345 to +360
def _account_metadata_fields(instance, data):
user = data.get("user", "")
host = data.get("host", "")
db_name = data.get("db_name", "")
if instance.db_type == "mysql" and not host:
user_host = data.get("user_host", "")
if user_host and "@`" in user_host:
try:
host = user_host.rsplit("@`", 1)[1].rstrip("`")
except IndexError:
host = ""
elif instance.db_type == "mongo" and not db_name:
db_name_user = data.get("db_name_user", "")
if "." in db_name_user:
db_name, user = db_name_user.split(".", 1)
return user, host, db_name
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Parse raw user@host values here too.

_mysql_user_host() accepts both `user`@`host` and raw user@host, but _account_metadata_fields() only back-fills host from the quoted form. For password-reset and delete flows that supply user_host without host, the live engine call succeeds while the metadata lookup uses host="", which can create a duplicate row on reset or leave stale metadata on delete.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/views.py` around lines 345 - 360, _account_metadata_fields
currently only extracts host from the quoted form "`user`@`host`", causing
missing host when callers pass raw "user@host"; update the MySQL branch in
_account_metadata_fields to handle both forms: read user_host =
data.get("user_host",""), if user_host contains "@`" keep existing rsplit logic,
else if user_host contains "@" split on the last "@" (user_host.rsplit("@",1)),
strip any surrounding backticks or quotes from both parts, and assign host (and
user if missing) so metadata lookups match _mysql_user_host and avoid
duplicate/stale rows.

Comment thread api_instances/views.py
Comment on lines +1138 to +1145
database_record = InstanceDatabase.objects.create(
instance=instance,
db_name=db_name,
owner=owner,
owner_display=owner_display,
remark=remark,
)
_clear_instance_resource_cache()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't use create() after the live database operation.

InstanceDatabase is unique on (instance, db_name), and InstanceOperationDatabaseDetail.put already allows metadata-only upserts for the same key. If a saved metadata row exists before the live create runs, this create() raises IntegrityError after the database has already been created on the instance, leaving the API in a partial-success state.

Proposed fix
-        database_record = InstanceDatabase.objects.create(
-            instance=instance,
-            db_name=db_name,
-            owner=owner,
-            owner_display=owner_display,
-            remark=remark,
-        )
+        database_record, _ = InstanceDatabase.objects.update_or_create(
+            instance=instance,
+            db_name=db_name,
+            defaults={
+                "owner": owner,
+                "owner_display": owner_display,
+                "remark": remark,
+            },
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
database_record = InstanceDatabase.objects.create(
instance=instance,
db_name=db_name,
owner=owner,
owner_display=owner_display,
remark=remark,
)
_clear_instance_resource_cache()
database_record, _ = InstanceDatabase.objects.update_or_create(
instance=instance,
db_name=db_name,
defaults={
"owner": owner,
"owner_display": owner_display,
"remark": remark,
},
)
_clear_instance_resource_cache()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/views.py` around lines 1138 - 1145, The code uses
InstanceDatabase.objects.create(...) after creating the live DB which can raise
IntegrityError if a metadata-only row already exists; replace the create() call
with an idempotent upsert such as
InstanceDatabase.objects.update_or_create((instance=instance, db_name=db_name),
defaults={'owner': owner, 'owner_display': owner_display, 'remark': remark}) or
equivalent get-or-create + update flow to avoid IntegrityError and ensure
existing metadata is updated; keep the subsequent
_clear_instance_resource_cache() call. Reference InstanceDatabase,
InstanceOperationDatabaseDetail.put, create(), and
_clear_instance_resource_cache().

Comment on lines +168 to +180
function movePage(direction: -1 | 1) {
const nextPage = filters.page + direction
if (nextPage < 1) {
return
}
filters.page = nextPage
void loadAuditLogs()
}

const debouncedLoadAuditLogs = useDebounceFn(() => {
filters.page = 1
void loadAuditLogs()
}, 250)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential pagination/debounce race.

movePage calls loadAuditLogs() synchronously while debouncedLoadAuditLogs (250ms) may have a pending call from a recent filter change. The pending debounced call will fire after the page change and reset to page = 1, overwriting the user's pagination. Consider cancelling the debounce before paginating, e.g., with useDebounceFn's returned cancel handle, or have movePage also flush/cancel pending loads.

♻️ Suggested approach
-const debouncedLoadAuditLogs = useDebounceFn(() => {
+const debouncedLoadAuditLogs = useDebounceFn(() => {
   filters.page = 1
   void loadAuditLogs()
 }, 250)

 function movePage(direction: -1 | 1) {
   const nextPage = filters.page + direction
   if (nextPage < 1) {
     return
   }
+  // Drop any pending debounced reset to avoid clobbering the new page.
   filters.page = nextPage
   void loadAuditLogs()
 }

useDebounceFn exposes a cancellable form via useDebounceFn(fn, ms, { rejectOnCancel: false }) plus .cancel(); alternatively, switch to a manually managed timer and clear it in movePage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/audit/pages/AuditPage.vue` around lines 168 - 180, The
pagination can be overwritten by a pending debounced filter load; modify
movePage to cancel or flush the debounced call before changing page so a pending
debounced handler doesn't reset filters.page to 1. Specifically, call the
cancel/flush method returned by useDebounceFn (refer to debouncedLoadAuditLogs)
or use the rejectOnCancel option and invoke
debouncedLoadAuditLogs.cancel()/flush() prior to setting filters.page and
calling loadAuditLogs(), ensuring loadAuditLogs() is the authoritative immediate
fetch after pagination.

Comment on lines +250 to +263
function accountPayload() {
if (!selectedInstanceId.value) {
throw new Error('Select an instance first.')
}

return {
instance_id: selectedInstanceId.value,
db_name: accountForm.dbName.trim(),
user: accountForm.user.trim(),
host: accountForm.host.trim(),
password: accountForm.password,
remark: accountForm.remark.trim(),
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the payload shape expected by updateInstanceOperationAccount.

frontend/src/lib/api.ts:1527-1560 builds the edit URL from payload.instanceId and payload.username, but accountPayload() returns instance_id and user. As written, edit submissions generate a URL with undefined path segments.

Also applies to: 286-297

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/AccountManagementPage.vue`
around lines 250 - 263, accountPayload() currently returns snake_case keys
instance_id and user but updateInstanceOperationAccount expects
payload.instanceId and payload.username when building the edit URL; change
accountPayload() (and the similar payload at lines 286-297) to return instanceId
(from selectedInstanceId.value) and username (from accountForm.user.trim())
along with the other fields (dbName, host, password, remark) so the API function
receives the correctly shaped payload and the edit URL is built without
undefined segments.

Comment on lines +157 to +162
try {
const response = await fetchInstanceOperationDatabases(requireToken(), {
instance_id: selectedInstanceId.value,
saved: savedOnly.value,
})
databases.value = response.results
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Match these calls to the frontend API helper contract.

frontend/src/lib/api.ts:1495-1510 expects fetchInstanceOperationDatabases(instanceId, token) and builds the update URL from payload.instanceId / payload.dbName. Here the load call passes (token, queryObject), and the edit payload only contains instance_id / db_name, so the list request uses the token as the instance ID and the edit request resolves its patch URL with undefined path params.

Also applies to: 189-201

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/DatabaseManagementPage.vue`
around lines 157 - 162, The list call is using the wrong argument order and
payload keys for the frontend API helper: fetchInstanceOperationDatabases
expects (instanceId, token) and the API payloads use camelCase fields
instanceId/dbName, but the page passes (token, { instance_id, db_name, saved })
which makes the token treated as the instanceId and leaves path params undefined
for edits. Fix by calling fetchInstanceOperationDatabases with
selectedInstanceId.value as the first arg and requireToken() as the second, and
update any edit/patch payloads (the create/edit calls on this page) to use
camelCase keys instanceId and dbName (and pass the instanceId value from
selectedInstanceId.value) so the helper can build the correct URLs.

Comment on lines +207 to +220
onMounted(async () => {
await authStore.loadCurrentUser()

if (!canOpenParamMenu.value) {
error.value = 'You do not have permission to open parameter settings.'
return
}
if (!canViewParams.value) {
error.value = 'You do not have permission to view instance parameters.'
return
}

await refreshAll()
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unhandled rejection on initial load (same pattern previously flagged in AuditPage.vue/DataDictionaryPage.vue).

authStore.loadCurrentUser() is awaited bare, so a network blip or expired refresh token leaves the user on an empty page with no surfaced error. The other pages in this PR wrap this in try/catch + toUserFacingMessage; do the same here for consistency.

🛡️ Suggested fix
 onMounted(async () => {
-  await authStore.loadCurrentUser()
+  try {
+    await authStore.loadCurrentUser()
+  } catch (errorValue) {
+    error.value = toUserFacingMessage(errorValue, 'Failed to load the current user.')
+    return
+  }

   if (!canOpenParamMenu.value) {
     error.value = 'You do not have permission to open parameter settings.'
     return
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onMounted(async () => {
await authStore.loadCurrentUser()
if (!canOpenParamMenu.value) {
error.value = 'You do not have permission to open parameter settings.'
return
}
if (!canViewParams.value) {
error.value = 'You do not have permission to view instance parameters.'
return
}
await refreshAll()
})
onMounted(async () => {
try {
await authStore.loadCurrentUser()
} catch (errorValue) {
error.value = toUserFacingMessage(errorValue, 'Failed to load the current user.')
return
}
if (!canOpenParamMenu.value) {
error.value = 'You do not have permission to open parameter settings.'
return
}
if (!canViewParams.value) {
error.value = 'You do not have permission to view instance parameters.'
return
}
await refreshAll()
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue`
around lines 207 - 220, The onMounted block awaits authStore.loadCurrentUser()
without error handling which can leave the page blank on network/token errors;
wrap the await in a try/catch, convert the caught error via toUserFacingMessage
and set error.value accordingly (then return) before proceeding to permission
checks and refreshAll; update the onMounted handler that calls
authStore.loadCurrentUser(), uses toUserFacingMessage, and ensures refreshAll is
only called on success.

Comment on lines +170 to +180
try {
if (activeTab.value === 'processes') {
rows.value = (await fetchInstanceOperationDiagnosticProcesses(requireToken(), {
instance_id: selectedInstanceId.value,
command_type: commandType.value,
})).results
} else if (activeTab.value === 'tablespace') {
rows.value = (await fetchInstanceOperationDiagnosticTablespace(requireToken(), {
instance_id: selectedInstanceId.value,
size: 25,
})).results
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Flip the diagnostic fetch arguments.

frontend/src/lib/api.ts:1612-1620 and 1643-1675 define fetchInstanceOperationDiagnosticProcesses(payload, token) and fetchInstanceOperationDiagnosticTablespace(payload, token). These calls are reversed, so the access token is sent as the payload and the auth token becomes [object Object]; the Processes and Tablespace tabs will fail to load.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue`
around lines 170 - 180, The diagnostic fetch calls are passing arguments in the
wrong order causing the token to be used as payload; update the calls where
activeTab === 'processes' and 'tablespace' to call
fetchInstanceOperationDiagnosticProcesses(payload, token) and
fetchInstanceOperationDiagnosticTablespace(payload, token) respectively (build
the payload using selectedInstanceId.value, commandType.value or size as before)
and pass requireToken() as the second argument so the payload (object) is the
first parameter and the auth token is the second; adjust the two call sites in
SessionDiagnosticsPage.vue accordingly.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
api_instances/views.py (1)

1118-1122: Legacy archery- collection name in Mongo placeholder.

The placeholder collection used to materialize a new Mongo database is hardcoded as f"archery-{db_name}", which leaks the legacy product name into freshly-created Mongo instances even though this PR is rebranding to Datamingle. If you do change it, note that any existing deployments depending on the old name will break — so a migration/compat plan is needed before flipping it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/views.py` around lines 1118 - 1122, Replace the hardcoded
legacy collection name "archery-{db_name}" with the new brand-safe name (e.g.,
"datamingle-{db_name}") where the Mongo placeholder is created in the block
checking instance.db_type == "mongo" (look for the variables/identifiers
exec_result, conn = engine.get_connection(), database = conn[db_name], and
database.create_collection). Update the string used in
database.create_collection to use the new prefix and ensure any surrounding
comments/logs that reference "archery" are renamed; also add a TODO or comment
near this change noting that a migration/compatibility plan is required for
deployments relying on the old collection name so it can be rolled out safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@sql/migrations/0015_alter_permission_options.py`:
- Around line 19-31: RETIRED_SQL_PERMISSION_NAMES contains two values that don't
match the originals from 0001_initial.py: update the entries for keys
"menu_document" and "sql_analyze" in RETIRED_SQL_PERMISSION_NAMES so they
exactly match the original display names ("Menu Related Documentation" and
"Execute SQL Analysis" respectively) so reverse migration (migrate sql 0014)
restores the exact prior name values; adjust the mapping in
sql/migrations/0015_alter_permission_options.py and re-run/verify the migration
rollback to ensure byte-equality with 0001_initial.

---

Nitpick comments:
In `@api_instances/views.py`:
- Around line 1118-1122: Replace the hardcoded legacy collection name
"archery-{db_name}" with the new brand-safe name (e.g., "datamingle-{db_name}")
where the Mongo placeholder is created in the block checking instance.db_type ==
"mongo" (look for the variables/identifiers exec_result, conn =
engine.get_connection(), database = conn[db_name], and
database.create_collection). Update the string used in
database.create_collection to use the new prefix and ensure any surrounding
comments/logs that reference "archery" are renamed; also add a TODO or comment
near this change noting that a migration/compatibility plan is required for
deployments relying on the old collection name so it can be rolled out safely.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3fe0f42-f628-4f82-b9f7-bbb226d9d24e

📥 Commits

Reviewing files that changed from the base of the PR and between 147f827 and 70cac8c.

📒 Files selected for processing (7)
  • api_instances/serializers.py
  • api_instances/views.py
  • frontend/src/features/instance-operations/pages/DatabaseManagementPage.vue
  • frontend/src/features/instance-operations/pages/ParameterSettingsPage.vue
  • frontend/src/features/inventory/pages/DataDictionaryPage.vue
  • sql/migrations/0015_alter_permission_options.py
  • sql/test_query_privileges.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/features/instance-operations/pages/DatabaseManagementPage.vue

Comment on lines +19 to +31
RETIRED_SQL_PERMISSION_NAMES = {
"menu_sqlanalyze": "Menu SQL Analysis",
"menu_sqloptimize": "Menu SQL Optimization",
"menu_sqladvisor": "Menu Optimization Tools",
"menu_slowquery": "Menu Slow Query Log",
"menu_my2sql": "Menu My2SQL",
"menu_schemasync": "Menu SchemaSync",
"menu_document": "Menu Documentation",
"sql_analyze": "Execute SQL analysis",
"optimize_sqladvisor": "Execute SQLAdvisor",
"optimize_sqltuning": "Execute SQLTuning",
"optimize_soar": "Execute SOAR",
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reverse migration restores wrong display names for two permissions.

Compared with the originals defined in sql/migrations/0001_initial.py, two entries in RETIRED_SQL_PERMISSION_NAMES don't match what the reverse path should restore:

  • menu_document: originally "Menu Related Documentation", here "Menu Documentation".
  • sql_analyze: originally "Execute SQL Analysis", here "Execute SQL analysis" (lowercase a).

A rollback (migrate sql 0014) will recreate the permissions with diverged name values, so the DB won't be byte-equal to pre-migration state. Codenames still match, so permission checks aren't affected — purely a fidelity issue for reverse migrations / admin display.

📝 Proposed fix to align names with 0001_initial
-    "menu_document": "Menu Documentation",
+    "menu_document": "Menu Related Documentation",
-    "sql_analyze": "Execute SQL analysis",
+    "sql_analyze": "Execute SQL Analysis",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RETIRED_SQL_PERMISSION_NAMES = {
"menu_sqlanalyze": "Menu SQL Analysis",
"menu_sqloptimize": "Menu SQL Optimization",
"menu_sqladvisor": "Menu Optimization Tools",
"menu_slowquery": "Menu Slow Query Log",
"menu_my2sql": "Menu My2SQL",
"menu_schemasync": "Menu SchemaSync",
"menu_document": "Menu Documentation",
"sql_analyze": "Execute SQL analysis",
"optimize_sqladvisor": "Execute SQLAdvisor",
"optimize_sqltuning": "Execute SQLTuning",
"optimize_soar": "Execute SOAR",
}
RETIRED_SQL_PERMISSION_NAMES = {
"menu_sqlanalyze": "Menu SQL Analysis",
"menu_sqloptimize": "Menu SQL Optimization",
"menu_sqladvisor": "Menu Optimization Tools",
"menu_slowquery": "Menu Slow Query Log",
"menu_my2sql": "Menu My2SQL",
"menu_schemasync": "Menu SchemaSync",
"menu_document": "Menu Related Documentation",
"sql_analyze": "Execute SQL Analysis",
"optimize_sqladvisor": "Execute SQLAdvisor",
"optimize_sqltuning": "Execute SQLTuning",
"optimize_soar": "Execute SOAR",
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/migrations/0015_alter_permission_options.py` around lines 19 - 31,
RETIRED_SQL_PERMISSION_NAMES contains two values that don't match the originals
from 0001_initial.py: update the entries for keys "menu_document" and
"sql_analyze" in RETIRED_SQL_PERMISSION_NAMES so they exactly match the original
display names ("Menu Related Documentation" and "Execute SQL Analysis"
respectively) so reverse migration (migrate sql 0014) restores the exact prior
name values; adjust the mapping in
sql/migrations/0015_alter_permission_options.py and re-run/verify the migration
rollback to ensure byte-equality with 0001_initial.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (6)
sql/migrations/0015_alter_permission_options.py (2)

26-27: ⚠️ Potential issue | 🟡 Minor

Reverse migration display names still diverge from 0001_initial.py.

The two values flagged previously remain unchanged. A migrate sql 0014 rollback will recreate menu_document and sql_analyze with names that don't byte-match the originals. Codename-based permission checks are unaffected, but admin display / fidelity is.

📝 Proposed fix
-    "menu_document": "Menu Documentation",
+    "menu_document": "Menu Related Documentation",
-    "sql_analyze": "Execute SQL analysis",
+    "sql_analyze": "Execute SQL Analysis",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/migrations/0015_alter_permission_options.py` around lines 26 - 27, The
reverse migration in 0015_alter_permission_options.py still uses altered display
strings for the "menu_document" and "sql_analyze" permission entries; update the
reverse mapping so that the display names for permission codenames
"menu_document" and "sql_analyze" exactly match the originals in 0001_initial.py
(use the identical byte-for-byte strings from 0001_initial.py) to ensure a
rollback (`migrate sql 0014`) recreates the same display names; locate the
reverse mapping logic in 0015_alter_permission_options.py that sets these keys
and replace their values with the original strings from 0001_initial.py.

5-17: ⚠️ Potential issue | 🟠 Major

Seed SQL still reinserts retired codenames — past finding not addressed.

The seed files under src/init_sql/v*.sql continue to INSERT the same codenames being removed here (menu_sqlanalyze, menu_sqloptimize, menu_sqladvisor, menu_slowquery, menu_my2sql, menu_schemasync, menu_document, sql_analyze, optimize_sqladvisor, optimize_sqltuning). On a fresh install where seeds run after migrations, the retired permissions get reintroduced and resurface in roles/UIs. Please drop those INSERTs from the seed SQL (or pin seed ordering to run before this migration).

#!/bin/bash
# Re-confirm the codenames are still present in seed SQL files.
fd -e sql . src/init_sql 2>/dev/null | xargs rg -nP '\b(menu_sqlanalyze|menu_sqloptimize|menu_sqladvisor|menu_slowquery|menu_my2sql|menu_schemasync|menu_document|sql_analyze|optimize_sqladvisor|optimize_sqltuning|optimize_soar)\b'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/migrations/0015_alter_permission_options.py` around lines 5 - 17, The
migration defines RETIRED_SQL_PERMISSION_CODENAMES that should not be
reintroduced by seed data; remove any INSERT statements in the seed SQL files
under src/init_sql/v*.sql that create permissions with the codenames listed in
RETIRED_SQL_PERMISSION_CODENAMES (menu_sqlanalyze, menu_sqloptimize,
menu_sqladvisor, menu_slowquery, menu_my2sql, menu_schemasync, menu_document,
sql_analyze, optimize_sqladvisor, optimize_sqltuning, optimize_soar), or
alternatively ensure seed execution is pinned to run before this migration;
update the seed SQL files to drop those INSERTs (or guard them) so the
migration’s retire list in RETIRED_SQL_PERMISSION_CODENAMES isn’t undone on
fresh installs.
frontend/src/features/audit/pages/AuditPage.vue (1)

168-180: ⚠️ Potential issue | 🟡 Minor

Pending debounced filter load can still clobber pagination.

movePage updates filters.page and calls loadAuditLogs() synchronously, but a recent filter edit may have queued debouncedLoadAuditLogs (250 ms). When that pending callback fires after the page change, it resets filters.page = 1 and reloads, undoing the navigation. Cancel/flush the debounced handle in movePage (e.g. switch to useDebounceFn(fn, 250, { rejectOnCancel: false }) and call .cancel() before paginating, or use a manually managed timer).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/audit/pages/AuditPage.vue` around lines 168 - 180, The
pagination can be clobbered by a pending debounced filter reload
(debouncedLoadAuditLogs) because movePage sets filters.page and immediately
calls loadAuditLogs while a queued debounce may later reset filters.page = 1;
fix this by cancelling or flushing the pending debounce inside movePage before
changing page—e.g., switch useDebounceFn to one that returns a cancel/flush
handle (or use useDebounceFn(fn, 250, { rejectOnCancel: false })) and call
debouncedLoadAuditLogs.cancel() (or .flush()) at the start of movePage so the
pending callback cannot reset filters.page, then proceed to set filters.page and
call loadAuditLogs.
api_instances/views.py (2)

1165-1171: ⚠️ Potential issue | 🟠 Major

Use an upsert after the live database create.

This still does a plain create() after the database has already been created on the instance. If a metadata row already exists for (instance, db_name), the API fails locally with IntegrityError even though the live database creation succeeded.

Suggested fix
-        database_record = InstanceDatabase.objects.create(
-            instance=instance,
-            db_name=db_name,
-            owner=owner,
-            owner_display=owner_display,
-            remark=remark,
-        )
+        database_record, _ = InstanceDatabase.objects.update_or_create(
+            instance=instance,
+            db_name=db_name,
+            defaults={
+                "owner": owner,
+                "owner_display": owner_display,
+                "remark": remark,
+            },
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/views.py` around lines 1165 - 1171, Replace the plain create()
call on InstanceDatabase with an upsert using
InstanceDatabase.objects.update_or_create so a preexisting metadata row for
(instance, db_name) is updated instead of raising IntegrityError; call
update_or_create with lookup kwargs instance=instance, db_name=db_name and pass
owner, owner_display, remark in the defaults mapping, and assign the returned
object to database_record.

366-381: ⚠️ Potential issue | 🟠 Major

Parse raw user@host values here as well.

_mysql_user_host() already accepts both quoted and raw MySQL account identifiers, but _account_metadata_fields() only extracts host from the quoted form. Reset/delete requests that send raw user@host will operate on the live account correctly while missing the saved metadata row.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/views.py` around lines 366 - 381, The function
_account_metadata_fields only parses quoted MySQL identifiers ("@`") but misses
raw user@host forms; update the mysql branch (where user_host is read) to also
handle the plain "@" case: if user_host contains "@`" keep the existing
rsplit("@`",1) logic, else if it contains "@" rsplit("@",1) to set host and user
(strip any surrounding backticks or quotes from both parts), and fall back to
empty on errors; reference _account_metadata_fields and the local user_host
variable when making the change.
api_instances/serializers.py (1)

154-164: ⚠️ Potential issue | 🟠 Major

Apply the non-blank password validator to reset requests too.

InstanceAccountPasswordSerializer still accepts whitespace-only passwords, so the reset endpoint can pass " " through as a “valid” password. This is the same gap that was fixed for account creation, but not for password resets.

Suggested fix
 class InstanceAccountPasswordSerializer(serializers.Serializer):
     instance_id = serializers.IntegerField()
     db_name = serializers.CharField(max_length=128, allow_blank=True, required=False)
     db_name_user = serializers.CharField(
         max_length=260, allow_blank=True, required=False
     )
     user_host = serializers.CharField(max_length=260, allow_blank=True, required=False)
     user = serializers.CharField(max_length=128)
     host = serializers.CharField(max_length=64, allow_blank=True, required=False)
     password = serializers.CharField(max_length=128, write_only=True)
+
+    def validate_password(self, value):
+        if not value or not value.strip():
+            raise serializers.ValidationError("Password cannot be blank.")
+        return value
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/serializers.py` around lines 154 - 164, The password field on
InstanceAccountPasswordSerializer currently allows whitespace-only strings;
change its validation to reject blank/whitespace-only passwords by removing
allow_blank=True for the password field and adding a validator that enforces
non-whitespace content (i.e., ensure the password CharField "password" in
InstanceAccountPasswordSerializer trims whitespace and fails if empty or only
whitespace). Update the serializer's password declaration to use
allow_blank=False (or add an explicit validator that checks str.strip() != "")
so reset requests are validated the same as account creation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api_instances/views.py`:
- Around line 801-808: Requests currently trust any db_name after instance
lookup; enforce the same visibility filtering used by DataDictionaryDatabaseList
before querying metadata. After calling _data_dictionary_instance(request.user,
instance_id) and before using get_engine/get_group_tables_by_db, validate the
requested db_name against the instance's show_db_name_regex and
denied_db_name_regex rules (the same logic used by DataDictionaryDatabaseList);
if the db_name is not allowed, raise the same not-found/permission response.
Apply this check around the handlers that use get_engine and
query_engine.get_group_tables_by_db (the block with escape_string and
get_group_tables_by_db and the other locations noted at 879-898 and 954-959).

In `@frontend/src/features/audit/pages/AuditPage.vue`:
- Around line 160-198: Tab changes are being debounced because setView mutates
watched filters and the watcher routes through debouncedLoadAuditLogs; fix by
making setView perform an immediate fetch and suppress the debounced watcher:
after updating activeView, filters.page and clearing filters in setView, call
void loadAuditLogs() directly and set a short-lived flag (e.g.,
suppressDebouncedLoad = true) that the existing watcher checks and returns early
if true; reset suppressDebouncedLoad (via nextTick or a timeout) so subsequent
filter edits still go through debouncedLoadAuditLogs. Ensure you reference
setView, loadAuditLogs, debouncedLoadAuditLogs and the watcher when applying the
change.

In `@frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue`:
- Around line 164-188: The diagnostics loader leaves previous rows visible
during fetch; clear the stale UI state by resetting rows before starting the
network call. In the function that contains this block (e.g., loadRows / the
function manipulating loadingRows.value, error.value, selectedThreadIds.value),
set rows.value = [] (or an appropriate empty state) immediately after setting
loadingRows.value = true and before the try/await calls so old rows are not
shown or used (and ensure selectedThreadIds.value is still cleared as shown).

In `@frontend/src/features/inventory/pages/DataDictionaryPage.vue`:
- Around line 161-176: The page keeps showing stale databases/tableGroups while
awaiting the new fetch; before calling fetchDataDictionaryDatabases (and
likewise before the table-fetch block around lines 185-200), immediately clear
the rendered data so the UI doesn't show the old instance: set databases.value =
[] and tableGroups.value = [] (and clear tableDetail.value,
selectedDbName.value/selectedTableName.value as you already do) before the
await, then on success assign databases.value = response.result and restore
selectedDbName; do the same pattern in the table loader (clear
tables/tableGroups/tableDetail before calling the fetch function) so failed
requests won't leave prior schema visible.

---

Duplicate comments:
In `@api_instances/serializers.py`:
- Around line 154-164: The password field on InstanceAccountPasswordSerializer
currently allows whitespace-only strings; change its validation to reject
blank/whitespace-only passwords by removing allow_blank=True for the password
field and adding a validator that enforces non-whitespace content (i.e., ensure
the password CharField "password" in InstanceAccountPasswordSerializer trims
whitespace and fails if empty or only whitespace). Update the serializer's
password declaration to use allow_blank=False (or add an explicit validator that
checks str.strip() != "") so reset requests are validated the same as account
creation.

In `@api_instances/views.py`:
- Around line 1165-1171: Replace the plain create() call on InstanceDatabase
with an upsert using InstanceDatabase.objects.update_or_create so a preexisting
metadata row for (instance, db_name) is updated instead of raising
IntegrityError; call update_or_create with lookup kwargs instance=instance,
db_name=db_name and pass owner, owner_display, remark in the defaults mapping,
and assign the returned object to database_record.
- Around line 366-381: The function _account_metadata_fields only parses quoted
MySQL identifiers ("@`") but misses raw user@host forms; update the mysql branch
(where user_host is read) to also handle the plain "@" case: if user_host
contains "@`" keep the existing rsplit("@`",1) logic, else if it contains "@"
rsplit("@",1) to set host and user (strip any surrounding backticks or quotes
from both parts), and fall back to empty on errors; reference
_account_metadata_fields and the local user_host variable when making the
change.

In `@frontend/src/features/audit/pages/AuditPage.vue`:
- Around line 168-180: The pagination can be clobbered by a pending debounced
filter reload (debouncedLoadAuditLogs) because movePage sets filters.page and
immediately calls loadAuditLogs while a queued debounce may later reset
filters.page = 1; fix this by cancelling or flushing the pending debounce inside
movePage before changing page—e.g., switch useDebounceFn to one that returns a
cancel/flush handle (or use useDebounceFn(fn, 250, { rejectOnCancel: false }))
and call debouncedLoadAuditLogs.cancel() (or .flush()) at the start of movePage
so the pending callback cannot reset filters.page, then proceed to set
filters.page and call loadAuditLogs.

In `@sql/migrations/0015_alter_permission_options.py`:
- Around line 26-27: The reverse migration in 0015_alter_permission_options.py
still uses altered display strings for the "menu_document" and "sql_analyze"
permission entries; update the reverse mapping so that the display names for
permission codenames "menu_document" and "sql_analyze" exactly match the
originals in 0001_initial.py (use the identical byte-for-byte strings from
0001_initial.py) to ensure a rollback (`migrate sql 0014`) recreates the same
display names; locate the reverse mapping logic in
0015_alter_permission_options.py that sets these keys and replace their values
with the original strings from 0001_initial.py.
- Around line 5-17: The migration defines RETIRED_SQL_PERMISSION_CODENAMES that
should not be reintroduced by seed data; remove any INSERT statements in the
seed SQL files under src/init_sql/v*.sql that create permissions with the
codenames listed in RETIRED_SQL_PERMISSION_CODENAMES (menu_sqlanalyze,
menu_sqloptimize, menu_sqladvisor, menu_slowquery, menu_my2sql, menu_schemasync,
menu_document, sql_analyze, optimize_sqladvisor, optimize_sqltuning,
optimize_soar), or alternatively ensure seed execution is pinned to run before
this migration; update the seed SQL files to drop those INSERTs (or guard them)
so the migration’s retire list in RETIRED_SQL_PERMISSION_CODENAMES isn’t undone
on fresh installs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 725772f6-2fbe-4d8d-8faf-b8e8dd996a1d

📥 Commits

Reviewing files that changed from the base of the PR and between 70cac8c and b43da8b.

📒 Files selected for processing (8)
  • api_instances/serializers.py
  • api_instances/tests.py
  • api_instances/views.py
  • docs/spa-bootstrap-parity-checklist.md
  • frontend/src/features/audit/pages/AuditPage.vue
  • frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue
  • frontend/src/features/inventory/pages/DataDictionaryPage.vue
  • sql/migrations/0015_alter_permission_options.py
✅ Files skipped from review due to trivial changes (1)
  • docs/spa-bootstrap-parity-checklist.md

Comment thread api_instances/views.py
Comment on lines +801 to +808
instance = _data_dictionary_instance(request.user, instance_id)

try:
query_engine = get_engine(instance=instance)
escaped_db_name = query_engine.escape_string(db_name)
grouped_tables = query_engine.get_group_tables_by_db(
db_name=escaped_db_name
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Enforce database visibility here, not just in the listing endpoint.

DataDictionaryDatabaseList filters databases through show_db_name_regex / denied_db_name_regex, but these handlers trust any requested db_name once the instance is visible. That lets a user query table metadata or export a schema that was intentionally hidden from the database list just by guessing its name.

Also applies to: 879-898, 954-959

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api_instances/views.py` around lines 801 - 808, Requests currently trust any
db_name after instance lookup; enforce the same visibility filtering used by
DataDictionaryDatabaseList before querying metadata. After calling
_data_dictionary_instance(request.user, instance_id) and before using
get_engine/get_group_tables_by_db, validate the requested db_name against the
instance's show_db_name_regex and denied_db_name_regex rules (the same logic
used by DataDictionaryDatabaseList); if the db_name is not allowed, raise the
same not-found/permission response. Apply this check around the handlers that
use get_engine and query_engine.get_group_tables_by_db (the block with
escape_string and get_group_tables_by_db and the other locations noted at
879-898 and 954-959).

Comment on lines +160 to +198
function setView(view: AuditView) {
activeView.value = view
filters.page = 1
filters.action = ''
filters.status = ''
filters.syntaxType = ''
}

function movePage(direction: -1 | 1) {
const nextPage = filters.page + direction
if (nextPage < 1 || (direction === 1 && activePage.value.next === null)) {
return
}
filters.page = nextPage
void loadAuditLogs()
}

const debouncedLoadAuditLogs = useDebounceFn(() => {
filters.page = 1
void loadAuditLogs()
}, 250)

watch(
() => [
activeView.value,
filters.search,
filters.action,
filters.status,
filters.syntaxType,
filters.startDate,
filters.endDate,
],
() => {
if (initializing) {
return
}
debouncedLoadAuditLogs()
},
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tab switches incur a 250 ms debounce delay.

setView mutates activeView and resets filters.action/status/syntaxType, all of which are watched. The watcher then routes through debouncedLoadAuditLogs, so clicking a tab waits ~250 ms before the fetch starts (and any in-flight pending debounced filter edit can also race with it). Per the AI summary, tab/view changes are supposed to refetch immediately while only filter edits should be debounced. Consider invoking loadAuditLogs() directly from setView (and skipping the watch when only activeView changed), or splitting the watch into a tab watcher (immediate) and a filter watcher (debounced).

♻️ Possible split
-watch(
-  () => [
-    activeView.value,
-    filters.search,
-    filters.action,
-    filters.status,
-    filters.syntaxType,
-    filters.startDate,
-    filters.endDate,
-  ],
-  () => {
-    if (initializing) {
-      return
-    }
-    debouncedLoadAuditLogs()
-  },
-)
+watch(
+  () => activeView.value,
+  () => {
+    if (initializing) return
+    filters.page = 1
+    void loadAuditLogs()
+  },
+)
+watch(
+  () => [
+    filters.search,
+    filters.action,
+    filters.status,
+    filters.syntaxType,
+    filters.startDate,
+    filters.endDate,
+  ],
+  () => {
+    if (initializing) return
+    debouncedLoadAuditLogs()
+  },
+)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function setView(view: AuditView) {
activeView.value = view
filters.page = 1
filters.action = ''
filters.status = ''
filters.syntaxType = ''
}
function movePage(direction: -1 | 1) {
const nextPage = filters.page + direction
if (nextPage < 1 || (direction === 1 && activePage.value.next === null)) {
return
}
filters.page = nextPage
void loadAuditLogs()
}
const debouncedLoadAuditLogs = useDebounceFn(() => {
filters.page = 1
void loadAuditLogs()
}, 250)
watch(
() => [
activeView.value,
filters.search,
filters.action,
filters.status,
filters.syntaxType,
filters.startDate,
filters.endDate,
],
() => {
if (initializing) {
return
}
debouncedLoadAuditLogs()
},
)
function setView(view: AuditView) {
activeView.value = view
filters.page = 1
filters.action = ''
filters.status = ''
filters.syntaxType = ''
}
function movePage(direction: -1 | 1) {
const nextPage = filters.page + direction
if (nextPage < 1 || (direction === 1 && activePage.value.next === null)) {
return
}
filters.page = nextPage
void loadAuditLogs()
}
const debouncedLoadAuditLogs = useDebounceFn(() => {
filters.page = 1
void loadAuditLogs()
}, 250)
watch(
() => activeView.value,
() => {
if (initializing) return
filters.page = 1
void loadAuditLogs()
},
)
watch(
() => [
filters.search,
filters.action,
filters.status,
filters.syntaxType,
filters.startDate,
filters.endDate,
],
() => {
if (initializing) return
debouncedLoadAuditLogs()
},
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/audit/pages/AuditPage.vue` around lines 160 - 198, Tab
changes are being debounced because setView mutates watched filters and the
watcher routes through debouncedLoadAuditLogs; fix by making setView perform an
immediate fetch and suppress the debounced watcher: after updating activeView,
filters.page and clearing filters in setView, call void loadAuditLogs() directly
and set a short-lived flag (e.g., suppressDebouncedLoad = true) that the
existing watcher checks and returns early if true; reset suppressDebouncedLoad
(via nextTick or a timeout) so subsequent filter edits still go through
debouncedLoadAuditLogs. Ensure you reference setView, loadAuditLogs,
debouncedLoadAuditLogs and the watcher when applying the change.

Comment on lines +164 to +188
loadingRows.value = true
error.value = ''
feedback.value = ''
selectedThreadIds.value = []
killSql.value = ''

try {
if (activeTab.value === 'processes') {
rows.value = (await fetchInstanceOperationDiagnosticProcesses(requireToken(), {
instance_id: selectedInstanceId.value,
command_type: commandType.value,
})).results
} else if (activeTab.value === 'tablespace') {
rows.value = (await fetchInstanceOperationDiagnosticTablespace(requireToken(), {
instance_id: selectedInstanceId.value,
size: 25,
})).results
} else if (activeTab.value === 'transactions') {
rows.value = (await fetchInstanceOperationDiagnosticTransactions(requireToken(), selectedInstanceId.value)).results
} else {
rows.value = (await fetchInstanceOperationDiagnosticLocks(requireToken(), selectedInstanceId.value)).results
}
} catch (errorValue) {
error.value = toUserFacingMessage(errorValue, 'Failed to load diagnostics.')
} finally {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear stale diagnostics rows before refetching.

loadRows() resets the selected thread IDs, but it leaves the previous rows visible until the new request succeeds. After switching instances, a failed or slow fetch can leave old process rows on screen under the new instance context, and the kill flow will then submit those IDs against selectedInstanceId.value.

Suggested fix
 async function loadRows() {
   if (!selectedInstanceId.value) {
     rows.value = []
     return
   }

   loadingRows.value = true
   error.value = ''
   feedback.value = ''
+  rows.value = []
   selectedThreadIds.value = []
   killSql.value = ''

   try {
     if (activeTab.value === 'processes') {
       rows.value = (await fetchInstanceOperationDiagnosticProcesses(requireToken(), {
@@
       rows.value = (await fetchInstanceOperationDiagnosticLocks(requireToken(), selectedInstanceId.value)).results
     }
   } catch (errorValue) {
+    rows.value = []
     error.value = toUserFacingMessage(errorValue, 'Failed to load diagnostics.')
   } finally {
     loadingRows.value = false
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/instance-operations/pages/SessionDiagnosticsPage.vue`
around lines 164 - 188, The diagnostics loader leaves previous rows visible
during fetch; clear the stale UI state by resetting rows before starting the
network call. In the function that contains this block (e.g., loadRows / the
function manipulating loadingRows.value, error.value, selectedThreadIds.value),
set rows.value = [] (or an appropriate empty state) immediately after setting
loadingRows.value = true and before the try/await calls so old rows are not
shown or used (and ensure selectedThreadIds.value is still cleared as shown).

Comment on lines +161 to +176
loadingDatabases.value = true
error.value = ''
selectedDbName.value = ''
selectedTableName.value = ''
tableGroups.value = []
tableDetail.value = null

try {
const response = await fetchDataDictionaryDatabases(selectedInstanceId.value, requireToken())
databases.value = response.result
selectedDbName.value = databases.value[0] ?? ''
} catch (errorValue) {
error.value = toUserFacingMessage(errorValue, 'Failed to load databases.')
} finally {
loadingDatabases.value = false
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Drop stale instance/database data before loading the next scope.

These loaders reset the selection, but they keep the previous databases / tableGroups rendered until the request completes. If the new request fails, the page keeps showing the old instance's schema, which is pretty misleading.

Suggested fix
 async function loadDatabases() {
   if (!selectedInstanceId.value) {
     databases.value = []
     return
   }

   loadingDatabases.value = true
   error.value = ''
+  databases.value = []
   selectedDbName.value = ''
   selectedTableName.value = ''
   tableGroups.value = []
   tableDetail.value = null
@@
 async function loadTables() {
   if (!selectedInstanceId.value || !selectedDbName.value) {
     tableGroups.value = []
     return
   }

   loadingTables.value = true
   error.value = ''
+  tableGroups.value = []
   selectedTableName.value = ''
   tableDetail.value = null

Also applies to: 185-200

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/features/inventory/pages/DataDictionaryPage.vue` around lines
161 - 176, The page keeps showing stale databases/tableGroups while awaiting the
new fetch; before calling fetchDataDictionaryDatabases (and likewise before the
table-fetch block around lines 185-200), immediately clear the rendered data so
the UI doesn't show the old instance: set databases.value = [] and
tableGroups.value = [] (and clear tableDetail.value,
selectedDbName.value/selectedTableName.value as you already do) before the
await, then on success assign databases.value = response.result and restore
selectedDbName; do the same pattern in the table loader (clear
tables/tableGroups/tableDetail before calling the fetch function) so failed
requests won't leave prior schema visible.

@jruszo jruszo merged commit a1f7fcd into master Apr 27, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants