From a4252db5c4ef111f849cafc6cbfa1f65cebbacea Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 03:35:45 +0000 Subject: [PATCH] =?UTF-8?q?fix(showcase,rest,runtime):=20resolve=20#2616?= =?UTF-8?q?=20findings=20=E2=80=94=20REST=20fields=20crash,=20AI=20404=20s?= =?UTF-8?q?pam,=20dead-end=20wizard,=20unbound=20Mark=20Done,=20stuck=20KP?= =?UTF-8?q?I=20cards,=20offline=20seed=20images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the framework-repo-scoped items from the #2616 browser UX pass: - REST: normalize a string `fields` query param (e.g. `?fields=name`) the same way `select` already is, plus a defensive array guard in the SQL driver — fixes "query.fields.map is not a function". - Runtime: `GET /ai/agents` now returns `200 {agents:[]}` instead of a 404 when no AI service is configured (the open-source default), instead of spamming the console on every navigation. - Showcase pages: - CRM Workbench / Account Cockpit: switched off the legacy `top`/`$filter` query aliases (removed in 11.0) to the canonical `limit`/`where` keys, and stopped silently swallowing fetch errors — the KPI cards had no way to explain why they were stuck at 0. - Review Queue: dropped `showcase_mark_done` from the page toolbar — it's a record-scoped action with a `visible` expression that has nothing to evaluate against with no bound record; it already surfaces correctly per-row via its existing `list_item` location. - New Project Wizard: added an explicit `submitBehavior` so a successful create swaps the filled step-3 form for a confirmation panel instead of leaving it open to an accidental duplicate submit. - Seed data: replaced picsum.photos task cover URLs with local data: URI placeholders — Gallery/All Views no longer show broken images offline. - Command Center: added `stepSize: 1` to count-based chart axes, and added the page to the e2e smoke suite with a tightened chart-width assertion so a repeat of the reported chart-collapse fails CI. Several findings from the same issue trace to the sibling objectui repo (sidebar i18n label override, ActionEngine visibility fail-open, the Overdue-on-start-date formatter, the MapLibre tile source, console-bundle Sentry retries, and the chart width-collapse rendering itself) — tracked in #2620 since that repo isn't in this session's scope. Fixes #2616 --- .../app-showcase/e2e/showcase-smoke.spec.ts | 5 ++- examples/app-showcase/src/data/index.ts | 31 +++++++++++++------ .../src/pages/account-cockpit.page.ts | 15 ++++++--- .../src/pages/command-center.page.ts | 19 ++++++++---- .../src/pages/crm-workbench.page.ts | 7 +++-- .../src/pages/new-project-wizard.page.ts | 5 +++ .../src/pages/review-queue.page.ts | 15 ++++++--- packages/metadata-protocol/src/protocol.ts | 10 ++++++ packages/plugins/driver-sql/src/sql-driver.ts | 2 +- packages/runtime/src/http-dispatcher.ts | 9 ++++++ 10 files changed, 88 insertions(+), 30 deletions(-) diff --git a/examples/app-showcase/e2e/showcase-smoke.spec.ts b/examples/app-showcase/e2e/showcase-smoke.spec.ts index 837853a7d5..fa19a92d6e 100644 --- a/examples/app-showcase/e2e/showcase-smoke.spec.ts +++ b/examples/app-showcase/e2e/showcase-smoke.spec.ts @@ -24,6 +24,7 @@ const SURFACES: { name: string; path: string; chart?: boolean }[] = [ { name: 'Field Zoo', path: base('showcase_field_zoo') }, { name: 'Delivery Operations', path: base('dashboard/showcase_ops_dashboard'), chart: true }, { name: 'Chart Gallery', path: base('dashboard/showcase_chart_gallery'), chart: true }, + { name: 'Command Center', path: base('page/showcase_command_center'), chart: true }, { name: 'Hours by Status', path: base('report/showcase_hours_by_status') }, { name: 'Status × Priority', path: base('report/showcase_status_priority_matrix') }, { name: 'Task Overview', path: base('report/showcase_task_overview') }, @@ -63,7 +64,9 @@ for (const surface of SURFACES) { await svg.waitFor({ state: 'visible', timeout: 25_000 }); const box = await svg.boundingBox(); expect(box, `${surface.name}: no chart SVG`).not.toBeNull(); - expect(box!.width, `${surface.name}: chart width`).toBeGreaterThan(0); + // >0 alone previously passed even on a collapsed ~130px-wide chart panel + // (#2616 D) — require a width a real chart panel would actually have. + expect(box!.width, `${surface.name}: chart width`).toBeGreaterThan(200); expect(box!.height, `${surface.name}: chart height`).toBeGreaterThan(0); } }); diff --git a/examples/app-showcase/src/data/index.ts b/examples/app-showcase/src/data/index.ts index 41b92c056d..e0f09c7308 100644 --- a/examples/app-showcase/src/data/index.ts +++ b/examples/app-showcase/src/data/index.ts @@ -19,6 +19,17 @@ import { FieldZoo } from '../objects/field-zoo.object.js'; * work location (map), and projects span every status and health. */ +/** + * Local, offline-safe placeholder cover image. Task `cover` seeds used to + * point at picsum.photos, which renders as a wall of broken images in + * offline/restricted-network environments (Gallery, All Views). A data: URI + * needs no network at all and still gives each card a distinct color + number. + */ +function placeholderCover(seed: number, color: string): string { + const svg = `${seed}`; + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +} + const accounts = defineSeed(Account, { mode: 'upsert', externalId: 'name', @@ -76,16 +87,16 @@ const tasks = defineSeed(Task, { mode: 'upsert', externalId: 'title', records: [ - { cover: 'https://picsum.photos/seed/showcasetask1/480/300', title: 'Audit current IA', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 8, progress: 100, done: true, created_at: cel`daysAgo(20)`, start_date: cel`daysAgo(20)`, end_date: cel`daysAgo(18)`, due_date: cel`daysAgo(18)`, location: { lat: 47.6062, lng: -122.3321 } }, - { cover: 'https://picsum.photos/seed/showcasetask2/480/300', title: 'Design system', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'in_review', priority: 'high', estimate_hours: 24, progress: 80, done: false, created_at: cel`daysAgo(14)`, start_date: cel`daysAgo(12)`, end_date: cel`daysFromNow(2)`, due_date: cel`daysFromNow(2)`, location: { lat: 37.7749, lng: -122.4194 } }, - { cover: 'https://picsum.photos/seed/showcasetask3/480/300', title: 'Build homepage', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'in_progress', priority: 'high', estimate_hours: 40, progress: 45, done: false, created_at: cel`daysAgo(8)`, start_date: cel`daysAgo(6)`, end_date: cel`daysFromNow(10)`, due_date: cel`daysFromNow(10)`, location: { lat: 40.7128, lng: -74.0060 } }, - { cover: 'https://picsum.photos/seed/showcasetask4/480/300', title: 'SEO migration plan', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'todo', priority: 'medium', estimate_hours: 16, progress: 0, done: false, created_at: cel`daysAgo(3)`, start_date: cel`daysFromNow(5)`, end_date: cel`daysFromNow(15)`, due_date: cel`daysFromNow(15)`, location: { lat: 30.2672, lng: -97.7431 } }, - { cover: 'https://picsum.photos/seed/showcasetask5/480/300', title: 'Content backlog', project: 'Website Relaunch', assignee: 'grace@example.com', status: 'backlog', priority: 'low', estimate_hours: 12, progress: 0, done: false, created_at: cel`daysAgo(2)`, due_date: cel`daysFromNow(30)`, location: { lat: 41.8781, lng: -87.6298 } }, - { cover: 'https://picsum.photos/seed/showcasetask6/480/300', title: 'Ingest pipeline', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_progress', priority: 'urgent', estimate_hours: 60, progress: 55, done: false, created_at: cel`daysAgo(40)`, start_date: cel`daysAgo(35)`, end_date: cel`daysFromNow(20)`, due_date: cel`daysFromNow(20)`, location: { lat: 39.7392, lng: -104.9903 } }, - { cover: 'https://picsum.photos/seed/showcasetask7/480/300', title: 'Warehouse schema', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_review', priority: 'high', estimate_hours: 30, progress: 90, done: false, created_at: cel`daysAgo(25)`, start_date: cel`daysAgo(22)`, end_date: cel`daysFromNow(3)`, due_date: cel`daysFromNow(3)`, location: { lat: 42.3601, lng: -71.0589 } }, - { cover: 'https://picsum.photos/seed/showcasetask8/480/300', title: 'PII access review', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'todo', priority: 'urgent', estimate_hours: 20, progress: 0, done: false, created_at: cel`daysAgo(5)`, start_date: cel`daysFromNow(2)`, end_date: cel`daysFromNow(12)`, due_date: cel`daysFromNow(12)`, location: { lat: 38.9072, lng: -77.0369 } }, - { cover: 'https://picsum.photos/seed/showcasetask9/480/300', title: 'Evidence collection', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'backlog', priority: 'medium', estimate_hours: 18, progress: 0, done: false, created_at: cel`daysAgo(1)`, due_date: cel`daysFromNow(25)`, location: { lat: 34.0522, lng: -118.2437 } }, - { cover: 'https://picsum.photos/seed/showcasetask10/480/300', title: 'App wireframes', project: 'Mobile App', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 16, progress: 100, done: true, created_at: cel`daysAgo(10)`, start_date: cel`daysAgo(10)`, end_date: cel`daysAgo(6)`, due_date: cel`daysAgo(6)`, location: { lat: 45.5152, lng: -122.6784 } }, + { cover: placeholderCover(1, '#10B981'), title: 'Audit current IA', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 8, progress: 100, done: true, created_at: cel`daysAgo(20)`, start_date: cel`daysAgo(20)`, end_date: cel`daysAgo(18)`, due_date: cel`daysAgo(18)`, location: { lat: 47.6062, lng: -122.3321 } }, + { cover: placeholderCover(2, '#8B5CF6'), title: 'Design system', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'in_review', priority: 'high', estimate_hours: 24, progress: 80, done: false, created_at: cel`daysAgo(14)`, start_date: cel`daysAgo(12)`, end_date: cel`daysFromNow(2)`, due_date: cel`daysFromNow(2)`, location: { lat: 37.7749, lng: -122.4194 } }, + { cover: placeholderCover(3, '#F59E0B'), title: 'Build homepage', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'in_progress', priority: 'high', estimate_hours: 40, progress: 45, done: false, created_at: cel`daysAgo(8)`, start_date: cel`daysAgo(6)`, end_date: cel`daysFromNow(10)`, due_date: cel`daysFromNow(10)`, location: { lat: 40.7128, lng: -74.0060 } }, + { cover: placeholderCover(4, '#3B82F6'), title: 'SEO migration plan', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'todo', priority: 'medium', estimate_hours: 16, progress: 0, done: false, created_at: cel`daysAgo(3)`, start_date: cel`daysFromNow(5)`, end_date: cel`daysFromNow(15)`, due_date: cel`daysFromNow(15)`, location: { lat: 30.2672, lng: -97.7431 } }, + { cover: placeholderCover(5, '#94A3B8'), title: 'Content backlog', project: 'Website Relaunch', assignee: 'grace@example.com', status: 'backlog', priority: 'low', estimate_hours: 12, progress: 0, done: false, created_at: cel`daysAgo(2)`, due_date: cel`daysFromNow(30)`, location: { lat: 41.8781, lng: -87.6298 } }, + { cover: placeholderCover(6, '#F59E0B'), title: 'Ingest pipeline', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_progress', priority: 'urgent', estimate_hours: 60, progress: 55, done: false, created_at: cel`daysAgo(40)`, start_date: cel`daysAgo(35)`, end_date: cel`daysFromNow(20)`, due_date: cel`daysFromNow(20)`, location: { lat: 39.7392, lng: -104.9903 } }, + { cover: placeholderCover(7, '#8B5CF6'), title: 'Warehouse schema', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_review', priority: 'high', estimate_hours: 30, progress: 90, done: false, created_at: cel`daysAgo(25)`, start_date: cel`daysAgo(22)`, end_date: cel`daysFromNow(3)`, due_date: cel`daysFromNow(3)`, location: { lat: 42.3601, lng: -71.0589 } }, + { cover: placeholderCover(8, '#3B82F6'), title: 'PII access review', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'todo', priority: 'urgent', estimate_hours: 20, progress: 0, done: false, created_at: cel`daysAgo(5)`, start_date: cel`daysFromNow(2)`, end_date: cel`daysFromNow(12)`, due_date: cel`daysFromNow(12)`, location: { lat: 38.9072, lng: -77.0369 } }, + { cover: placeholderCover(9, '#94A3B8'), title: 'Evidence collection', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'backlog', priority: 'medium', estimate_hours: 18, progress: 0, done: false, created_at: cel`daysAgo(1)`, due_date: cel`daysFromNow(25)`, location: { lat: 34.0522, lng: -118.2437 } }, + { cover: placeholderCover(10, '#10B981'), title: 'App wireframes', project: 'Mobile App', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 16, progress: 100, done: true, created_at: cel`daysAgo(10)`, start_date: cel`daysAgo(10)`, end_date: cel`daysAgo(6)`, due_date: cel`daysAgo(6)`, location: { lat: 45.5152, lng: -122.6784 } }, ], }); diff --git a/examples/app-showcase/src/pages/account-cockpit.page.ts b/examples/app-showcase/src/pages/account-cockpit.page.ts index dd7d7d2fcb..ca616a66a3 100644 --- a/examples/app-showcase/src/pages/account-cockpit.page.ts +++ b/examples/app-showcase/src/pages/account-cockpit.page.ts @@ -28,11 +28,16 @@ function Page() { let alive = true; (async () => { if (!adapter || !sel) { setRelated({ projects: 0, invoices: 0, openInvoices: 0 }); return; } - const pr = await adapter.find('showcase_project', { $filter: ['account', '=', sel.id], top: 500 }); - const iv = await adapter.find('showcase_invoice', { $filter: ['account', '=', sel.id], top: 500 }); - const projects = Array.isArray(pr) ? pr : (pr && pr.records) || []; - const invoices = Array.isArray(iv) ? iv : (iv && iv.records) || []; - if (alive) setRelated({ projects: projects.length, invoices: invoices.length, openInvoices: invoices.filter((r) => r.status !== 'paid' && r.status !== 'void').length }); + try { + // Canonical QueryOptionsV2 keys only (where + limit, not the legacy + // $filter/top aliases removed in 11.0) — an unrecognized param here + // would leave the rollup cards silently stuck at 0. + const pr = await adapter.find('showcase_project', { where: { account: sel.id }, limit: 500 }); + const iv = await adapter.find('showcase_invoice', { where: { account: sel.id }, limit: 500 }); + const projects = Array.isArray(pr) ? pr : (pr && pr.records) || []; + const invoices = Array.isArray(iv) ? iv : (iv && iv.records) || []; + if (alive) setRelated({ projects: projects.length, invoices: invoices.length, openInvoices: invoices.filter((r) => r.status !== 'paid' && r.status !== 'void').length }); + } catch (e) { console.warn('[Account Cockpit] failed to load related rollups', e); } })(); return () => { alive = false; }; }, [adapter, sel, reload]); diff --git a/examples/app-showcase/src/pages/command-center.page.ts b/examples/app-showcase/src/pages/command-center.page.ts index db98986a6c..465c2eddbc 100644 --- a/examples/app-showcase/src/pages/command-center.page.ts +++ b/examples/app-showcase/src/pages/command-center.page.ts @@ -84,9 +84,16 @@ function kpi(id: string, object: string, label: string, colorVariant: string, ag }; } -/** A dataset-bound chart with the shared brand palette. */ -function chart(id: string, chartType: string, dataset: string, dimensions: string[], values: string[]): any { - return { id, type: 'object-chart', responsiveStyles: { large: { width: '100%', minWidth: '0' } }, properties: { dataset, dimensions, values, chartType, colors: PALETTE } }; +/** + * A dataset-bound chart with the shared brand palette. `integerYAxis` pins the + * tick step to 1 — without it, count aggregates (small integers like 0-5) + * render fractional gridlines (0.8, 2.3, …) on the default Recharts axis. + */ +function chart(id: string, chartType: string, dataset: string, dimensions: string[], values: string[], integerYAxis?: boolean): any { + return { + id, type: 'object-chart', responsiveStyles: { large: { width: '100%', minWidth: '0' } }, + properties: { dataset, dimensions, values, chartType, colors: PALETTE, ...(integerYAxis ? { yAxis: [{ field: values[0], stepSize: 1 }] } : {}) }, + }; } const CHART_H = '376px'; @@ -148,14 +155,14 @@ export const CommandCenterPage = definePage({ // ── Row A — three equal chart panels ───────────────────────── band('cc_rowA', 3, [ - panel({ id: 'cc_status', title: '任务状态分布', accent: A.c1, minHeight: CHART_H, child: chart('cc_status_c', 'bar', 'showcase_task_metrics', ['status'], ['task_count']) }), + panel({ id: 'cc_status', title: '任务状态分布', accent: A.c1, minHeight: CHART_H, child: chart('cc_status_c', 'bar', 'showcase_task_metrics', ['status'], ['task_count'], true) }), panel({ id: 'cc_health', title: '项目健康度', accent: A.c4, minHeight: CHART_H, child: chart('cc_health_c', 'donut', 'showcase_project_metrics', ['health'], ['project_count']) }), - panel({ id: 'cc_priority', title: '优先级分布', accent: A.c5, minHeight: CHART_H, child: chart('cc_pri_c', 'bar', 'showcase_task_metrics', ['priority'], ['task_count']) }), + panel({ id: 'cc_priority', title: '优先级分布', accent: A.c5, minHeight: CHART_H, child: chart('cc_pri_c', 'bar', 'showcase_task_metrics', ['priority'], ['task_count'], true) }), ]), // ── Row B — wide trend (span 2) + spend ────────────────────── band('cc_rowB', 3, [ - panel({ id: 'cc_throughput', title: '任务吞吐趋势 (月)', accent: A.c2, span: 'span 2', minHeight: CHART_H, child: chart('cc_thr_c', 'area', 'showcase_task_metrics', ['created_at'], ['task_count']) }), + panel({ id: 'cc_throughput', title: '任务吞吐趋势 (月)', accent: A.c2, span: 'span 2', minHeight: CHART_H, child: chart('cc_thr_c', 'area', 'showcase_task_metrics', ['created_at'], ['task_count'], true) }), panel({ id: 'cc_budget', title: '预算 vs 支出 (按客户)', accent: A.c4, minHeight: CHART_H, child: chart('cc_bud_c', 'bar', 'showcase_project_metrics', ['account'], ['budget_sum', 'spent_sum']) }), ]), diff --git a/examples/app-showcase/src/pages/crm-workbench.page.ts b/examples/app-showcase/src/pages/crm-workbench.page.ts index bfeea1906c..98de23e88e 100644 --- a/examples/app-showcase/src/pages/crm-workbench.page.ts +++ b/examples/app-showcase/src/pages/crm-workbench.page.ts @@ -31,10 +31,13 @@ function Page() { const refreshStats = React.useCallback(async () => { if (!adapter) return; try { - const all = await adapter.find('showcase_project', { top: 200 }); + // Canonical QueryOptionsV2 keys only (limit, not the legacy top alias + // removed in 11.0) — the KPI cards silently stuck at 0 whenever adapter.find + // rejected on the unrecognized param, and the empty catch hid the failure. + const all = await adapter.find('showcase_project', { limit: 200 }); const rows = Array.isArray(all) ? all : (all && all.records) || []; setStats({ total: rows.length, active: rows.filter((r) => r.status === 'active').length }); - } catch (e) { /* ignore in demo */ } + } catch (e) { console.warn('[CRM Workbench] failed to refresh stats', e); } }, [adapter]); React.useEffect(() => { refreshStats(); }, [refreshStats, reloadKey]); diff --git a/examples/app-showcase/src/pages/new-project-wizard.page.ts b/examples/app-showcase/src/pages/new-project-wizard.page.ts index a158dbcd35..2b9152c93e 100644 --- a/examples/app-showcase/src/pages/new-project-wizard.page.ts +++ b/examples/app-showcase/src/pages/new-project-wizard.page.ts @@ -35,6 +35,11 @@ export const NewProjectWizardPage = definePage({ { label: 'Status & Health', description: 'Where does it stand today?', fields: ['status', 'health'] }, { label: 'Budget & Schedule', description: 'Money and dates.', fields: ['budget', 'spent', 'start_date', 'end_date'] }, ], + // Without this, a successful submit left the filled step-3 form in + // place with only a toast — re-clicking "Create" duplicated the + // record. `thank-you` swaps the form for a confirmation panel so + // there's nothing left to resubmit. + submitBehavior: { kind: 'thank-you', title: 'Project created', message: 'Your new project is ready — find it in Projects, or reopen this wizard to start another.' }, }, }, ], diff --git a/examples/app-showcase/src/pages/review-queue.page.ts b/examples/app-showcase/src/pages/review-queue.page.ts index 756abc14b6..691de46445 100644 --- a/examples/app-showcase/src/pages/review-queue.page.ts +++ b/examples/app-showcase/src/pages/review-queue.page.ts @@ -8,9 +8,13 @@ import { definePage } from '@objectstack/spec/ui'; * sees what is waiting on them; this is that surface. * * An interface (list) page over tasks currently `in_review` — the work - * awaiting a decision — with the source object's actions surfaced as toolbar - * buttons (Mark done = approve & complete) and a drawer to inspect each item. - * Tabs let the reviewer pivot to urgent or blocked work. + * awaiting a decision — with a drawer to inspect each item. "Mark Done" is + * deliberately NOT wired as a page-level `buttons:` toolbar entry: that + * surface has no bound record, so `MarkDoneAction`'s `visible: '!record.done'` + * expression has nothing to evaluate against and the button would render + * regardless of state. `MarkDoneAction.locations` already includes + * `list_item`, so it correctly appears per-row (with that row's record bound) + * instead. Tabs let the reviewer pivot to urgent or blocked work. */ export const ReviewQueuePage = definePage({ name: 'showcase_review_queue', @@ -29,8 +33,9 @@ export const ReviewQueuePage = definePage({ sort: [{ field: 'due_date', order: 'asc' }], appearance: { showDescription: true, allowedVisualizations: ['grid'] }, userActions: { sort: true, search: true, filter: false, rowHeight: false, addRecordForm: false }, - // Object actions as toolbar buttons — "approve & complete" the reviewed item. - buttons: ['showcase_mark_done'], + // No `buttons:` entry here — see file header comment: "Mark Done" is a + // record-scoped action and belongs per-row (`list_item`), not as an + // unbound page-toolbar button. // Click a row → drawer with the full record to review before deciding. recordAction: 'drawer', showRecordCount: true, diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 018b084e11..75776d6c1c 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -2203,6 +2203,16 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { } if (options.select !== undefined) delete options.select; + // fields: comma-separated string → array. Clients may pass `?fields=name` + // directly (not only via the `?select=` alias above) — a single-value + // querystring param arrives as a bare string, which drivers' `.map()` + // calls over `query.fields` would otherwise throw on. + if (typeof options.fields === 'string') { + options.fields = options.fields.split(',').map((s: string) => s.trim()).filter(Boolean); + } else if (options.fields !== undefined && !Array.isArray(options.fields)) { + delete options.fields; + } + // Sort/orderBy → orderBy: string → SortNode[] array const sortValue = options.orderBy ?? options.sort; if (typeof sortValue === 'string') { diff --git a/packages/plugins/driver-sql/src/sql-driver.ts b/packages/plugins/driver-sql/src/sql-driver.ts index 6eea543e9d..f6995f4d71 100644 --- a/packages/plugins/driver-sql/src/sql-driver.ts +++ b/packages/plugins/driver-sql/src/sql-driver.ts @@ -633,7 +633,7 @@ export class SqlDriver implements IDataDriver { const builder = buildBase(); // SELECT - if (query.fields) { + if (Array.isArray(query.fields) && query.fields.length > 0) { builder.select((query.fields as string[]).map((f: string) => this.mapSortField(f))); } else { builder.select('*'); diff --git a/packages/runtime/src/http-dispatcher.ts b/packages/runtime/src/http-dispatcher.ts index 412e8b98b4..cb45311003 100644 --- a/packages/runtime/src/http-dispatcher.ts +++ b/packages/runtime/src/http-dispatcher.ts @@ -3280,6 +3280,15 @@ export class HttpDispatcher { } if (!aiService) { + // The console polls `GET /ai/agents` on every navigation to decide + // whether to show AI affordances. Reporting that as a 404 turns the + // normal "no AI service configured" state (the open-source default — + // service-ai is a Cloud/Enterprise package) into console error-log + // spam on every page. An empty list conveys the same information + // without looking like a fault. Every other /ai/* route still 404s. + if (method === 'GET' && subPath === '/ai/agents') { + return { handled: true, response: { status: 200, body: { agents: [] } } }; + } return { handled: true, response: {