Skip to content

(MOT-4361) feat(database,console): writable sql panel, driver decode fixes, page-wide consistency, shared config dialog - #713

Merged
andersonleal merged 2 commits into
mainfrom
feat/database-console-overhaul
Aug 5, 2026
Merged

(MOT-4361) feat(database,console): writable sql panel, driver decode fixes, page-wide consistency, shared config dialog#713
andersonleal merged 2 commits into
mainfrom
feat/database-console-overhaul

Conversation

@andersonleal

@andersonleal andersonleal commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Fixes MOT-4361

The database console page overhaul from the 2026-08-04/05 live review sessions: two driver-level bugs, the sql panel gaining sanctioned writes, a page-wide consistency migration, and the console sharing its worker-configuration dialog with injected pages.

Worker (Rust)

  • MySQL type names: result columns reported the protocol enum's debug names (MYSQL_TYPE_VAR_STRING) instead of SQL names (varchar) — leaked into grid headers and every agent reading columns[].type, and silently broke the UI's type-category logic. New sql_type_name() maps the wire type (charset 63 splits text/binary twins, UNSIGNED honoured). Verified against the live mysql.
  • Postgres "char" decode: the row decoder had no arm for the internal one-byte "char" type (OID 18) and the unknown-type fallback's String FromSql rejects it — so listTables (which reads pg_class.relkind) failed on every postgres server with error deserializing column 2. New arm decodes i8→text. Verified against live postgres 18.4 and via ad-hoc SELECT relkind.

SQL panel

  • Writes allowed (owner decision): isReadOnlySql routes instead of gates — reads → database::query, writes → database::execute, the same surfaces agents use, so engine policy and database::row-changed treat both alike. Trailing ; stripped for the prepared paths.
  • DDL severity + honest account: drop/truncate/alter get an alert-ink note naming the change before ⌘⏎ ("drops table x — commits on mysql") and a past-tense echo after ("dropped table x") replacing MySQL's misleading 0 affected.
  • errText: unwraps the SDK's wire-error envelope to the driver's own code+message — every failure used to render [object Object].

Page-wide consistency (impeccable critiques: sql 27/40, page 25/40)

  • Keep-mounted panels: sql drafts, data filters/sort/cursor, the changes feed, and diagram zoom/drag all survive mode switches; header refresh threads refreshToken into fetchers instead of remount keys (and now actually reaches health, where it was a silent no-op).
  • One visual system: shared .db-toolbar bar grammar, derived AA warn/alert inks in both themes (raw tokens sit ~3:1 at 11px), page-wide :focus-visible hairline, ghost→faint on functional text, :focus-within beside hover-only reveals.
  • A11y: live regions (sql status, changes role="log", data rows/page), freshness badge became a real disclosure button (its caveat was hover-only title text), aria-expanded on tree/plan twists, role="grid" legitimizing the roving-cursor gridcells, keyboard hints in the data footer, Escape exits the Monaco tab-trap, ERD nodes take F to focus.
  • Table rail: collapsible (remembered per browser), type-to-filter, universal schema prefix stripped from display (public. ×14 was eating the name width), passive-mode notes on sql/health, list tagged by source db — a failed database switch can no longer render the previous database's tables under the new header.
  • Health: replication daemons labeled + demoted instead of billed as active queries at 13233.0s; durations roll up (3h 40m); as-of stamp; stale-dim while refetching.
  • Inspector: header in the shared bar grammar with an explicit close — a cell cursor used to pin the panel open with no way out — and its bottom rule aligns with the filter bar (fixing a class collision with RowDetail's standalone header).
  • Copy voice: plural guards ("1 tables"), interpunct idiom, pager caps, empty states with real actions.

Console: shared configuration dialog

WorkerConfigurationDialog joins the curated component registry, the package manifest, and the regenerated vendor shim (conformance-test pinned). The database page opens the console's own config editor in place via a host.components runtime lookup — dialog chrome, dirty guard, custom-form resolution and save all stay host-owned. Consoles predating the export degrade to the workers-tab navigation; the runtime lookup (never a static import) is what keeps an old console from failing the page's module load. Also: CodeEditor's completions separator is a visible '\n' now, replacing an invisible U+0001 that read as join('') in every editor and grep.

Verification

  • cargo fmt / clippy --all-features / cargo test green (347+28+3+1); driver fixes proven with throwaway integration tests against the live mysql and postgres 18.4
  • database/ui: tsc clean, 32/32 vitest (routing, ddlInfo, errText, commonSchema), biome clean per touched file
  • console: tsc -b clean, conformance test 3/3 (registry ↔ manifest ↔ types), shim regenerated (34 components)
  • Live-verified in the running console throughout: contrast/focus probes, keep-mounted behavior, footer alignment measured to the pixel

Notes for reviewers

  • The sql-panel write capability is deliberate ("a heads-up, not a gate") — no confirmation step; DDL gets the louder treatment described above.
  • .impeccable/critique/ snapshots and the provider Cargo.lock drift are intentionally not part of this PR.
  • Version skew is handled on both axes: older worker → console page hides missing modes with a hint; older console → configure falls back to navigation.

Summary by CodeRabbit

  • New Features
    • Added worker configuration access from the database console.
    • SQL panels now support write and DDL statements, with affected-row, insert-ID, and result reporting.
    • Added schema/table filtering, starter SQL, refresh controls, persisted navigation, and improved panel state retention.
    • Added keyboard shortcuts and accessibility enhancements across database views.
  • Bug Fixes
    • Improved database error messages and handling of MySQL, PostgreSQL, and autocomplete results.
    • Corrected stale data, refresh behavior, type metadata, and active-query display.
  • Style
    • Refined responsive layouts, toolbars, colors, focus states, and loading indicators.

… overhaul

Worker:
- mysql result columns report SQL type names (varchar, int) instead of
  the protocol enum's debug names (MYSQL_TYPE_VAR_STRING)
- postgres row decoder handles the internal one-byte "char" type
  (OID 18): listTables read pg_class.relkind and failed on every
  postgres server with 'error deserializing column 2'

SQL panel:
- writes allowed: reads route through database::query, writes through
  database::execute — the same surfaces agents use, so engine policy
  and database::row-changed see both alike
- DDL (drop/truncate/alter) gets an alert-ink note naming the change
  before it runs and a past-tense echo after ('dropped table x')
  instead of MySQL's truthful-but-misleading '0 affected'
- errText unwraps the SDK's wire-error envelope; every failure used to
  render '[object Object]'

Page-wide consistency (from the impeccable critique pair):
- user-work panels keep-mounted across mode switches; header refresh
  threads a token into fetchers instead of remount keys, so it can no
  longer wipe filter drafts
- shared toolbar grammar, derived AA warn/alert inks in both themes,
  page-wide focus-visible, live regions (sql status, changes role=log,
  data row/page announcements), focus-within beside hover reveals
- collapsible table rail with type-to-filter; universal schema prefix
  stripped from display; health labels+demotes replication daemons and
  rolls durations up to minutes/hours; table list tagged by source db
  so a failed switch can't render the previous database's schema
- inspector header in the shared bar grammar with an explicit close
  (a cell cursor used to pin the panel open with no way out)
- in-place configuration via the console's shared dialog when the
  running console exports it, workers-tab navigation as the fallback
The workers-page configuration editor joins the curated component
registry, the package manifest, and the regenerated vendor shim — the
first page-level composite in the kit. A worker page can now offer
'configure' in place (via a host.components runtime lookup) instead of
deep-linking to the workers tab and stranding the operator on the
workers list when the editor closes. Consoles predating the export
degrade to the navigation path; reading the registry at runtime rather
than importing the name is what keeps an old console from failing the
page's module load.

Also: CodeEditor's completions separator becomes a visible '\n' — it
was an invisible U+0001 that renders as an empty string in editors,
greps, and diffs, and cost a design review a false 'autocomplete is
broken' P1 before a live probe disproved it.
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 5, 2026 2:45pm
workers-tech-spec Ready Ready Preview Aug 5, 2026 2:45pm

Request Review

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 54 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change exposes WorkerConfigurationDialog, normalizes database result metadata, adds writable SQL execution, improves database-page state management, and updates database-panel accessibility, refresh behavior, filtering, and styling.

Changes

Console component surface

Layer / File(s) Summary
Worker configuration dialog export
packages/console-ui/index.d.ts, packages/console-ui/component-names.mjs, console/web/src/lib/console-api.ts, console/web/public/vendor/console-ui.js, console/web/src/lib/console-ui-conformance.test.ts
Adds the dialog contract and exposes WorkerConfigurationDialog through the component registry and conformance checks.
Completion delimiter parsing
console/web/src/components/ui/CodeEditor.tsx
Uses newline delimiters for completion serialization and parsing.

Database driver metadata

Layer / File(s) Summary
SQL type normalization
database/src/driver/mysql.rs, database/src/driver/postgres.rs, database/src/ui.rs
MySQL metadata uses SQL-style names, and PostgreSQL internal CHAR values decode as text. The database page documentation describes query reads and execute writes.

Database console

Layer / File(s) Summary
SQL execution contracts and routing
database/ui/src/lib/rpc.ts, database/ui/src/page/db-data.ts, database/ui/src/page/run-sql.test.ts
Adds database::execute, routes read and write SQL separately, returns write metadata, generates DDL descriptions, detects common schemas, and tests these paths.
Shared error formatting
database/ui/src/lib/errors.ts, database/ui/src/lib/errors.test.ts, database/ui/src/configuration/index.tsx, database/ui/src/page/useDatabaseRead.ts
Adds errText for structured host-call errors and uses it in configuration tests and database reads.
Database page orchestration
database/ui/src/page/index.tsx, database/ui/src/page/icons.tsx
Integrates worker configuration, persisted navigation, starter SQL, retained panels, refresh tokens, stale-result protection, and panel selection behavior.
Database panel behavior
database/ui/src/page/SqlPanel.tsx, database/ui/src/page/TableDataPanel.tsx, database/ui/src/page/SchemaTree.tsx, database/ui/src/page/HealthPanel.tsx, database/ui/src/page/ChangesPanel.tsx, database/ui/src/page/ErdPanel.tsx, database/ui/src/page/PlanTree.tsx, database/ui/src/page/result-grid.tsx, database/ui/src/page/useRowChanges.ts, database/ui/src/page/pagination.tsx
Updates SQL writes, refresh behavior, filtering, keyboard interaction, ARIA state, health reporting, change logs, ERD controls, result grids, and row sequencing.
Database console styling
database/ui/styles.css
Adds responsive layouts, retained-panel styling, focus states, toolbar rules, stale states, accessibility content, and adjusted colors.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SqlPanel
  participant runAdhocSql
  participant DatabaseRPC
  participant DatabasePage
  User->>SqlPanel: Submit SQL
  SqlPanel->>runAdhocSql: Route SQL by statement type
  runAdhocSql->>DatabaseRPC: Call database::query or database::execute
  DatabaseRPC-->>runAdhocSql: Return rows or write metadata
  runAdhocSql-->>SqlPanel: Return AdhocResult
  SqlPanel->>DatabasePage: Notify successful write
  DatabasePage->>SqlPanel: Refresh retained panel state
Loading

Possibly related PRs

Suggested reviewers: ytallo

Poem

A rabbit clicks “execute” with care,
While rows and fresh panels fill the air.
A dialog opens, schemas align,
Errors become messages clear and fine.
The console hops with state held tight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main database, driver, UI consistency, and shared configuration changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/database-console-overhaul

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
database/ui/styles.css (1)

857-858: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

.db-erd-stat.quiet repeats the base color.

Both rules set color: var(--color-ink-faint), so the quiet modifier changes nothing. Give it a dimmer value or remove the rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/ui/styles.css` around lines 857 - 858, Update the
`.db-erd-stat.quiet` rule so it has a visibly dimmer color than the base
`.db-erd-stat` style, or remove the redundant modifier rule if no distinct quiet
appearance is needed.
database/ui/src/page/index.tsx (1)

484-512: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Mount the retained diagram and changes panels on first visit.

The wrappers at Lines 484-512 render on the first page load, so ErdPanel runs its schemaDiagram read and ChangesPanel opens its row-change subscription even when the user stays on the data tab. The diagram layout is the most expensive worker call on this page.

Track visited modes and render each retained panel only after its first visit. State retention still works, because the panel then stays mounted.

♻️ Proposed change: gate the first mount on a visited set
+  // A retained panel keeps its state after the first visit; it does not need
+  // to pay for its first read before that visit happens.
+  const [visited, setVisited] = useState<Set<PanelMode>>(() => new Set(['data']))
+  useEffect(() => {
+    setVisited((cur) => (cur.has(mode) ? cur : new Set(cur).add(mode)))
+  }, [mode])
-                {modes.includes('diagram') ? (
+                {modes.includes('diagram') && visited.has('diagram') ? (
                   <div className="db-keep" hidden={mode !== 'diagram'}>
-                {modes.includes('changes') ? (
+                {modes.includes('changes') && visited.has('changes') ? (
                   <div className="db-keep" hidden={mode !== 'changes'}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/ui/src/page/index.tsx` around lines 484 - 512, Update the page
component’s mode-retention logic to track which modes have been visited, and
gate the retained ErdPanel and ChangesPanel wrappers on that visited-mode state.
Mark the active mode visited when it is first entered, while keeping each panel
mounted after its initial visit so its state and subscriptions are retained
during later mode switches.
database/ui/src/page/db-data.ts (1)

203-211: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the routing docblock onto isReadOnlySql.

The block at Lines 203-208 documents the read check, but it now sits directly above stripSqlComments. Attach it to isReadOnlySql and give stripSqlComments its own one-line description.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/ui/src/page/db-data.ts` around lines 203 - 211, Move the existing
routing explanation docblock from above stripSqlComments to immediately above
isReadOnlySql. Add a concise one-line description directly above
stripSqlComments identifying it as the SQL-comment removal helper, without
changing either function’s behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@console/web/src/components/ui/CodeEditor.tsx`:
- Around line 213-221: Update the completionsKey serialization used by
CodeEditor so arbitrary completion strings, including those containing newline
characters, round-trip without splitting into multiple labels. Replace the
delimiter-based join/split flow with JSON or another collision-free encoding,
while preserving the existing filtering and completion update behavior in the
effect.

In `@console/web/src/lib/console-api.ts`:
- Around line 93-96: Update WorkerConfigurationDialog before exposing it through
Host.components so configurationId is constrained to the current owner/tab
context or explicitly authorized. Validate the identifier before every
configuration load and save, rejecting unauthorized targets rather than passing
the untrusted value through. Keep the shared editor available only for
authorized configurations; freezing Host.components alone is insufficient.

In `@database/ui/src/lib/errors.ts`:
- Around line 42-48: Update unwrap to parse JSON only when current.message
begins with the handler error: envelope prefix, preserving ordinary messages and
error metadata even when they contain JSON; add a regression test covering a
non-wrapper message containing JSON.

In `@database/ui/src/page/db-data.ts`:
- Around line 209-219: Update stripSqlComments and the isReadOnlySql flow to
mask single- and double-quoted string literals with placeholders during the same
preprocessing pass as comment removal, preserving the surrounding SQL structure
for keyword checks. Ensure WRITE_ANYWHERE does not match keywords inside
literals while existing comment stripping and read-only validation remain
intact.

In `@database/ui/src/page/result-grid.tsx`:
- Around line 203-205: Update the row rendering in the table grid so its
keyboard handlers and tabIndex are applied only when keyboard is absent; when
keyboard is enabled, leave rows without Tab stops while preserving the grid cell
roving behavior and existing row-click handling.

---

Nitpick comments:
In `@database/ui/src/page/db-data.ts`:
- Around line 203-211: Move the existing routing explanation docblock from above
stripSqlComments to immediately above isReadOnlySql. Add a concise one-line
description directly above stripSqlComments identifying it as the SQL-comment
removal helper, without changing either function’s behavior.

In `@database/ui/src/page/index.tsx`:
- Around line 484-512: Update the page component’s mode-retention logic to track
which modes have been visited, and gate the retained ErdPanel and ChangesPanel
wrappers on that visited-mode state. Mark the active mode visited when it is
first entered, while keeping each panel mounted after its initial visit so its
state and subscriptions are retained during later mode switches.

In `@database/ui/styles.css`:
- Around line 857-858: Update the `.db-erd-stat.quiet` rule so it has a visibly
dimmer color than the base `.db-erd-stat` style, or remove the redundant
modifier rule if no distinct quiet appearance is needed.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f390288c-17d4-46ee-8c47-eb0d12d758ae

📥 Commits

Reviewing files that changed from the base of the PR and between c3dafba and 986ad59.

📒 Files selected for processing (29)
  • console/web/public/vendor/console-ui.js
  • console/web/src/components/ui/CodeEditor.tsx
  • console/web/src/lib/console-api.ts
  • console/web/src/lib/console-ui-conformance.test.ts
  • database/src/driver/mysql.rs
  • database/src/driver/postgres.rs
  • database/src/ui.rs
  • database/ui/src/configuration/index.tsx
  • database/ui/src/lib/errors.test.ts
  • database/ui/src/lib/errors.ts
  • database/ui/src/lib/rpc.ts
  • database/ui/src/page/ChangesPanel.tsx
  • database/ui/src/page/ErdPanel.tsx
  • database/ui/src/page/HealthPanel.tsx
  • database/ui/src/page/PlanTree.tsx
  • database/ui/src/page/SchemaTree.tsx
  • database/ui/src/page/SqlPanel.tsx
  • database/ui/src/page/TableDataPanel.tsx
  • database/ui/src/page/db-data.ts
  • database/ui/src/page/icons.tsx
  • database/ui/src/page/index.tsx
  • database/ui/src/page/pagination.tsx
  • database/ui/src/page/result-grid.tsx
  • database/ui/src/page/run-sql.test.ts
  • database/ui/src/page/useDatabaseRead.ts
  • database/ui/src/page/useRowChanges.ts
  • database/ui/styles.css
  • packages/console-ui/component-names.mjs
  • packages/console-ui/index.d.ts

Comment on lines +213 to +221
// '\n' as separator: identifiers can't contain it, and unlike the
// invisible control character it replaced, it can't masquerade as an
// empty string in an editor or a grep.
const completionsKey = (completions ?? []).join('\n')
React.useEffect(() => {
const editor = editorRef.current
if (!ready || !editor) return
const words = completionsKey
? completionsKey.split('').filter(Boolean)
? completionsKey.split('\n').filter(Boolean)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files matching target:"
fd -a 'CodeEditor\.tsx|SqlPanel\.tsx' . || true

echo
echo "CodeEditor relevant lines:"
if [ -f console/web/src/components/ui/CodeEditor.tsx ]; then
  sed -n '180,245p' console/web/src/components/ui/CodeEditor.tsx | cat -n
fi

echo
echo "SqlPanel references to CodeEditor completions:"
fd -a 'SqlPanel\.tsx' . | while read -r f; do
  echo "--- ${f#"$PWD/"}"
  rg -n "completion|Completions|CodeEditor|table|schema" "$f" || true
done

echo
echo "Direct usages of CodeEditor completions prop in repo:"
rg -n "<CodeEditor|CodeEditor\(" . -g '*.tsx' -g '*.ts' || true

Repository: iii-hq/workers

Length of output: 5587


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "SqlPanel completions construction and CodeEditor usage:"
sed -n '90,120p' database/ui/src/page/SqlPanel.tsx | cat -n
sed -n '200,214p' database/ui/src/page/SqlPanel.tsx | cat -n

echo
echo "All strings passed into CodeEditor completions prop (static source analysis):"
python3 - <<'PY'
from pathlib import Path
import re

for path in Path('.').rglob('*'):
    if not path.is_file():
        continue
    if path.suffix not in {'.tsx','.ts'}:
        continue
    text = path.read_text(errors='ignore').splitlines()
    for i,line in enumerate(text,1):
        m = re.search(r'\bcompletions\s*=\s*(\S+)', line)
        if m:
            print(f"{path}:{i}: {line.strip()}")
PY

echo
echo "Type declaration for CodeEditor completions prop:"
rg -n "completions\\?:" packages/console-ui/ -g '*.ts' -g '*.tsx' || true
sed -n '370,390p' packages/console-ui/index.d.ts 2>/dev/null || true

Repository: iii-hq/workers

Length of output: 3860


Use lossless completion serialization.

completions is a readonly string[], and SqlPanel passes database table names into it. If a table name contains \n, CodeEditor joins and splits with \n, so that entry produces two completion labels. Use JSON or another collision-free encoding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/components/ui/CodeEditor.tsx` around lines 213 - 221, Update
the completionsKey serialization used by CodeEditor so arbitrary completion
strings, including those containing newline characters, round-trip without
splitting into multiple labels. Replace the delimiter-based join/split flow with
JSON or another collision-free encoding, while preserving the existing filtering
and completion update behavior in the effect.

Comment on lines +93 to +96
// The one page-level composite in the kit: worker pages offer "configure"
// in place instead of navigating to the workers tab and stranding the
// operator there when the editor closes.
WorkerConfigurationDialog,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files matching console-api/database page/workers/config:"
git ls-files | rg '(^|/)(console-api|index\.tsx|.*config|.*worker|.*configuration).*' || true

echo
echo "console/web/src/lib/console-api.ts outline:"
if [ -f console/web/src/lib/console-api.ts ]; then
  ast-grep outline console/web/src/lib/console-api.ts || true
  echo
  sed -n '1,180p' console/web/src/lib/console-api.ts | cat -n
fi

echo
echo "database/ui/src/page/index.tsx relevant lines:"
if [ -f database/ui/src/page/index.tsx ]; then
  sed -n '60,280p' database/ui/src/page/index.tsx | cat -n
fi

echo
echo "Search WorkerConfigurationDialog definition/usages:"
rg -n "WorkerConfigurationDialog|ConfigurationDialog|configurationId|ConfigurationSave|saveConfiguration|configuration|configure" console web database -S || true

Repository: iii-hq/workers

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "WorkerConfigurationDialog component:"
fd -a 'WorkerConfigurationDialog' console/web/src/pages console/web/src/lib | sed 's#^\./##' || true
for f in $(fd 'WorkerConfigurationDialog' console/web/src/pages console/web/src/lib); do
  echo "--- $f"
  wc -l "$f"
  ast-grep outline "$f" || true
  sed -n '1,280p' "$f" | cat -n
done

echo
echo "ConsoleApi and Injectable Iii host components/types:"
rg -n "export interface ConsoleApi|interface ConsoleApi|type Host|Components|components|buildConsoleApi|ExtensionIii|register\(configurationId|WorkerConfiguration" console/web/src -S
files=$(rg -l "ConsoleApi|ExtensionIii|buildConsoleApi|components" console/web/src)
for f in $files; do
  echo "--- $f"
  sed -n '1,220p' "$f" | cat -n
done

Repository: iii-hq/workers

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate focused files with exact names only:"
for d in console/web/src/pages console/web/src/lib; do
  echo "-- $d"
  find "$d" -maxdepth 3 \( -path '*WorkerConfigurationDialog*' -o -path '*injectable*' -o -path '*console-api*' \) -print | sort
done

echo
echo "Find WorkerConfigurationDialog using rg exact filename:"
rg -n "export function WorkerConfigurationDialog|function WorkerConfigurationDialog|WorkerConfigurationDialog" console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' | head -80

echo
echo "Find injectable UI page component:"
rg -n "export interface Inject|interface Inject|function Injectable|console-ui|api\.components|buildConsoleApi|workerId|currentWorker|functionId" console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' | head -200

echo
echo "ConsoleApi type definitions:"
fd -a '.*\.ts$' console/web/src/types console/web/src | while read -r f; do
  if rg -q "ConsoleApi|ExtensionIii|components" "$f"; then
    echo "--- $f"
    cat -n "$f" | sed -n '1,220p'
  fi
done

echo
echo "Relevant package metadata:"
for f in console/web/package.json package.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    cat "$f" | sed -n '1,220p'
  fi
done

Repository: iii-hq/workers

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Targeted searches for the exact components/types referenced in this PR.
printf 'Exact symbol searches:\n'
rg -n --fixed-strings 'WorkerConfigurationDialog' console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' || true

printf '\nconsole-api exports/building:\n'
rg -n --fixed-strings 'console-api' console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' || true

printf '\nHost/components interfaces:\n'
rg -n 'interface ConsoleApi|type ConsoleApi|ConsoleApi|components:' console/web/src/types console/web/src/lib console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' || true

printf '\nConsoleApi usage by filename only:\n'
rg -n --fixed-strings 'ConsoleApi' console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' || true

Repository: iii-hq/workers

Length of output: 5015


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "console/web/src/types/injectable-ui.ts:"
sed -n '1,240p' console/web/src/types/injectable-ui.ts | cat -n

echo
echo "console/web/src/lib/ui-loader.tsx:"
sed -n '1,190p' console/web/src/lib/ui-loader.tsx | cat -n

echo
echo "console/web/src/pages/Workers/components/WorkerConfigurationDialog.tsx:"
sed -n '1,240p' console/web/src/pages/Workers/components/WorkerConfigurationDialog.tsx | cat -n

echo
echo "console/web/src/pages/Workers/index.tsx relevant:"
sed -n '1,130p' console/web/src/pages/Workers/index.tsx | cat -n

echo
echo "database/ui/src/page/index.tsx render/usage:"
sed -n '220,310p' database/ui/src/page/index.tsx | cat -n

echo
echo "console/web/src/main.tsx boot global:"
sed -n '1,70p' console/web/src/main.tsx | cat -n

echo
echo "console/web/src/pages/Workers/components/WorkerConfigurationDialog.tsx usages of configurationId/client:"
rg -n "configurationId|configuration::(list|get|set)|register|api|host|onClose|configuration" console/web/src/pages/Workers/components/WorkerConfigurationDialog.tsx

Repository: iii-hq/workers

Length of output: 28786


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "WorkerEditor and schema hook files containing configurationId/save/list:"
rg -n "WorkerEditor|useConfigurationSchema|configuration::list|configuration::set|configurationId" console/web/src/pages/Configuration console/web/src/hooks console/web/src/lib --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' | head -200

for f in \
  console/web/src/pages/Configuration/tabs/WorkersTab/hooks/useConfigurationSchema.ts \
  console/web/src/pages/Configuration/tabs/WorkersTab/WorkerEditor.tsx \
  console/web/src/hooks/use-hash-route.ts
do
  if [ -f "$f" ]; then
    echo
    echo "--- $f"
    ast-grep outline "$f" || true
    sed -n '1,260p' "$f" | cat -n
  fi
done

Repository: iii-hq/workers

Length of output: 29979


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "console/web/src/pages/Configuration/tabs/WorkersTab/api.ts:"
for f in \
  console/web/src/pages/Configuration/tabs/WorkersTab/api.ts \
  console/web/src/pages/Configuration/tabs/WorkersTab/hooks.ts
do
  echo "--- $f"
  ast-grep outline "$f" || true
  sed -n '1,180p' "$f" | cat -n
done

echo
echo "console/web/src/lib/console-config.ts:"
sed -n '1,110p' console/web/src/lib/console-config.ts | cat -n

echo
echo "configuration schema view and value caching:"
rg -n "type ConfigurationSchemaView|interface ConfigurationSchemaView|ConfigurationValue|Cache|invalidate|useConfigurationValue|setConfiguration" console/web/src/pages/Configuration/tabs/WorkersTab console/web/src/lib --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' | head -200

Repository: iii-hq/workers

Length of output: 16050


🌐 Web query:

iii-hq workers configuration::set configuration::list authorization id caller worker

💡 Result:

In the III (iii-hq) ecosystem, the configuration system is managed by the "configuration" worker [1]. The commands configuration::set and configuration::list are part of the internal trigger-based API used to manage worker settings [1][2]. 1. configuration::list: This command enumerates all registered configuration entries [1]. It returns an array of objects containing the ID, name, description, and schema of each configuration, but importantly does not return the stored values themselves [1][2]. To retrieve a specific value, you must use configuration::get with the appropriate ID [1][2]. 2. configuration::set: This command replaces the stored value for a registered configuration ID [1]. It validates the provided value against the registered JSON schema and fires a configuration:updated event, which allows other workers to react to the change [1]. Regarding the "authorization id" and "caller worker" context: - Authorization: RBAC (Role-Based Access Control) and authorization in III are primarily handled by the iii-worker-manager worker [3][4]. When configured with an RBAC block, you specify an auth_function_id (a function invoked per connection to authorize the worker) [3][4]. - Caller Worker: The term "caller worker" refers to a worker that initiates a request (e.g., calling another function or worker) [5]. There is no specific command called "authorization id caller worker"; instead, authorization is governed by the listener configuration and the auth_function_id function that validates the caller's context [4]. If you are attempting to configure these settings, you generally use the iii CLI to trigger these configuration functions or edit the backing files (typically YAML files under ./data/configuration/) which the configuration worker watches for updates [1][6][7]. Top results: [1][3][4][6]

Citations:


🌐 Web query:

site:github.com/iii-hq/workers configuration::get configuration::set caller_worker_id configuration worker

💡 Result:

The configuration::get and configuration::set functions are part of the built-in configuration worker in the iii workers system, which provides a centralized, schema-validated, and observable bus for managing worker configurations [1][2]. configuration::get This function reads a registered configuration entry by its identifier [3]. It is typically used by workers at boot time to fetch authoritative values or during runtime to refresh settings [4][5]. Example (Rust): trigger_with_retry( iii, "configuration::get", json!({ "id": CONFIG_ID }), timeout_ms,).await configuration::set This function updates or replaces the configuration value for a specific registered identifier [3]. It validates the provided value against the JSON schema previously registered for that ID [3]. Note that configuration::set replaces the entire value, meaning callers should perform a read-modify-write cycle if they only intend to update a portion of the configuration [6]. Example (TypeScript): await client.trigger('configuration::set', { id: CONSOLE_CONFIG_ID, value, }) Regarding caller_worker_id: The iii-sdk environment automatically injects metadata fields, such as _caller_worker_id, into every iii.trigger payload [7]. Developers should ensure their handler structures are designed to tolerate these engine-injected fields (for example, by not using strict Serde attributes like #[serde(deny_unknown_fields)] on function input types) to prevent deserialization errors [1][7].

Citations:


IDOR (CWE-639): Authorization Bypass Through User-Controlled Key (IDOR)

Reachability: Internal · Exploitability: Moderate

Guard WorkerConfigurationDialog inputs before exposing the shared config editor.

Host.components lets injected pages render this dialog. It accepts configurationId directly and then calls configuration load/save with that untrusted ID. Determine the target from owner/tab context, or authorize the ID before each read or write. Object.freeze(Host.components) does not restrict available identifiers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@console/web/src/lib/console-api.ts` around lines 93 - 96, Update
WorkerConfigurationDialog before exposing it through Host.components so
configurationId is constrained to the current owner/tab context or explicitly
authorized. Validate the identifier before every configuration load and save,
rejecting unauthorized targets rather than passing the untrusted value through.
Keep the shared editor available only for authorized configurations; freezing
Host.components alone is insufficient.

Comment on lines +42 to +48
const text = current.message
if (!text) return current
const brace = text.indexOf('{')
if (brace === -1) return current
let parsed: unknown
try {
parsed = JSON.parse(text.slice(brace))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Only unwrap handler error: envelopes.

unwrap parses any message that contains {. An ordinary driver error such as request failed: {"message":"context"} loses its original code and message.

Check for the handler error: prefix before parsing. Add a regression test for a non-wrapper message that contains JSON.

Proposed fix
-    const brace = text.indexOf('{')
-    if (brace === -1) return current
+    const prefix = 'handler error:'
+    if (!text.startsWith(prefix)) return current
+    const json = text.slice(prefix.length).trimStart()
+    if (!json.startsWith('{')) return current
     let parsed: unknown
     try {
-      parsed = JSON.parse(text.slice(brace))
+      parsed = JSON.parse(json)
📝 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
const text = current.message
if (!text) return current
const brace = text.indexOf('{')
if (brace === -1) return current
let parsed: unknown
try {
parsed = JSON.parse(text.slice(brace))
const text = current.message
if (!text) return current
const prefix = 'handler error:'
if (!text.startsWith(prefix)) return current
const json = text.slice(prefix.length).trimStart()
if (!json.startsWith('{')) return current
let parsed: unknown
try {
parsed = JSON.parse(json)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/ui/src/lib/errors.ts` around lines 42 - 48, Update unwrap to parse
JSON only when current.message begins with the handler error: envelope prefix,
preserving ordinary messages and error metadata even when they contain JSON; add
a regression test covering a non-wrapper message containing JSON.

Comment on lines +209 to 219
function stripSqlComments(sql: string): string {
return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '')
}

export function isReadOnlySql(sql: string): boolean {
const stripped = sql
.replace(/--[^\n]*/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '')
.trim()
const stripped = stripSqlComments(sql).trim()
if (!stripped) return false
if (stripped.replace(/;+\s*$/, '').includes(';')) return false
if (!READ_ONLY_LEAD.test(stripped)) return false
if (/^pragma\b/i.test(stripped) && stripped.includes('=')) return false
return !WRITE_ANYWHERE.test(stripped)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mask string literals before the keyword scan.

WRITE_ANYWHERE matches inside quoted literals. A read statement such as select * from t where status = 'update' is therefore routed to database::execute. The panel then shows the write notice and renders the execute-shaped outcome (0 affected) instead of the row grid, so a plain SELECT looks like it returned nothing.

Replace quoted spans with a placeholder in the same pass that strips comments.

🐛 Proposed fix: strip comments and mask literals
-function stripSqlComments(sql: string): string {
-  return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '')
-}
+function stripSqlComments(sql: string): string {
+  return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '')
+}
+
+/** Keyword scanning must not read the contents of a literal. */
+function maskLiterals(sql: string): string {
+  return sql.replace(/'(?:''|[^'])*'/g, "''").replace(/"(?:""|[^"])*"/g, '""')
+}
 
 export function isReadOnlySql(sql: string): boolean {
-  const stripped = stripSqlComments(sql).trim()
+  const stripped = maskLiterals(stripSqlComments(sql)).trim()
📝 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 stripSqlComments(sql: string): string {
return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '')
}
export function isReadOnlySql(sql: string): boolean {
const stripped = sql
.replace(/--[^\n]*/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '')
.trim()
const stripped = stripSqlComments(sql).trim()
if (!stripped) return false
if (stripped.replace(/;+\s*$/, '').includes(';')) return false
if (!READ_ONLY_LEAD.test(stripped)) return false
if (/^pragma\b/i.test(stripped) && stripped.includes('=')) return false
return !WRITE_ANYWHERE.test(stripped)
function stripSqlComments(sql: string): string {
return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '')
}
/** Keyword scanning must not read the contents of a literal. */
function maskLiterals(sql: string): string {
return sql.replace(/'(?:''|[^'])*'/g, "''").replace(/"(?:""|[^"])*"/g, '""')
}
export function isReadOnlySql(sql: string): boolean {
const stripped = maskLiterals(stripSqlComments(sql)).trim()
if (!stripped) return false
if (stripped.replace(/;+\s*$/, '').includes(';')) return false
if (!READ_ONLY_LEAD.test(stripped)) return false
if (/^pragma\b/i.test(stripped) && stripped.includes('=')) return false
return !WRITE_ANYWHERE.test(stripped)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/ui/src/page/db-data.ts` around lines 209 - 219, Update
stripSqlComments and the isReadOnlySql flow to mask single- and double-quoted
string literals with placeholders during the same preprocessing pass as comment
removal, preserving the surrounding SQL structure for keyword checks. Ensure
WRITE_ANYWHERE does not match keywords inside literals while existing comment
stripping and read-only validation remain intact.

Comment on lines +203 to +205
{/* With the roving cursor the table is a grid for real — the role
makes the per-cell `gridcell`s valid instead of orphaned. */}
<table className="db-grid" role={keyboard ? 'grid' : undefined}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove row Tab stops when keyboard is set.

TableDataPanel passes both keyboard and onRowClick. Each row then receives tabIndex={0} while a grid cell is also the roving Tab stop. Keyboard users must tab through every row before leaving the grid.

Apply the row keyboard handlers and tabIndex only when keyboard is absent.

Proposed fix
- tabIndex={onRowClick ? 0 : undefined}
+ tabIndex={onRowClick && !keyboard ? 0 : undefined}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/ui/src/page/result-grid.tsx` around lines 203 - 205, Update the row
rendering in the table grid so its keyboard handlers and tabIndex are applied
only when keyboard is absent; when keyboard is enabled, leave rows without Tab
stops while preserving the grid cell roving behavior and existing row-click
handling.

@andersonleal
andersonleal merged commit 29cda4b into main Aug 5, 2026
22 checks passed
@andersonleal
andersonleal deleted the feat/database-console-overhaul branch August 5, 2026 14:59
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.

1 participant