diff --git a/experimental/apps-mcp/cmd/init_template.go b/experimental/apps-mcp/cmd/init_template.go index 92a4b2c8ba..7ee8f7937c 100644 --- a/experimental/apps-mcp/cmd/init_template.go +++ b/experimental/apps-mcp/cmd/init_template.go @@ -273,6 +273,13 @@ After initialization: configFile := tmpFile.Name() + // Create output directory if specified and doesn't exist + if outputDir != "" { + if err := os.MkdirAll(outputDir, 0o755); err != nil { + return fmt.Errorf("create output directory: %w", err) + } + } + r := template.Resolver{ TemplatePathOrUrl: templatePathOrUrl, ConfigFile: configFile, diff --git a/experimental/apps-mcp/lib/prompts/apps.tmpl b/experimental/apps-mcp/lib/prompts/apps.tmpl index 1988a4268f..9f9b710488 100644 --- a/experimental/apps-mcp/lib/prompts/apps.tmpl +++ b/experimental/apps-mcp/lib/prompts/apps.tmpl @@ -23,10 +23,9 @@ invoke_databricks_cli 'experimental apps-mcp tools validate ./your-app-location' # Deployment -⚠️ Always use the sequence of commands: +⚠️ Use the deploy command which validates, deploys, and runs the app: -invoke_databricks_cli 'bundle deploy' -invoke_databricks_cli 'bundle run app' +invoke_databricks_cli 'experimental apps-mcp tools deploy' # View and manage your app: diff --git a/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/CLAUDE.md b/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/CLAUDE.md index 8a0ee216d0..2984603e28 100644 --- a/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/CLAUDE.md +++ b/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/CLAUDE.md @@ -21,7 +21,11 @@ The init-template command validates this automatically. ## TypeScript Import Rules -This template uses strict TypeScript settings with `verbatimModuleSyntax: true`. **Always use `import type` for type-only imports**: +This template uses strict TypeScript settings with `verbatimModuleSyntax: true`. **Always use `import type` for type-only imports**. + +Template enforces `noUnusedLocals` - remove unused imports immediately or build fails. + +**Type-only imports**: ```typescript // ✅ CORRECT - use import type for types @@ -166,6 +170,20 @@ WHERE DATE(timestamp_column) >= :start_date - **Dates**: Format as `YYYY-MM-DD`, use with `DATE()` in SQL - **Optional**: Use empty string default, check with `(:param = '' OR column = :param)` +## SQL Type Handling + +Numeric fields from Databricks SQL (especially `ROUND()`, `AVG()`, `SUM()`) are returned as strings in JSON. Convert before using numeric methods: + +```typescript +// ❌ WRONG - fails at runtime +{row.total_amount.toFixed(2)} + +// ✅ CORRECT +{Number(row.total_amount).toFixed(2)} +``` + +Use helpers from `shared/types.ts`: `toNumber()`, `formatCurrency()`, `formatPercent()`. + ## tRPC for Custom Endpoints: **CRITICAL**: Do NOT use tRPC for SQL queries or data retrieval. Use `config/queries/` + `useAnalyticsQuery` instead. @@ -363,6 +381,10 @@ npm run test:e2e:ui # Run with Playwright UI - Feature components: `client/src/components/FeatureName.tsx` - Split components when logic exceeds ~100 lines or component is reused +### Radix UI Constraints + +- `SelectItem` cannot have `value=""`. Use sentinel value like `"all"` for "show all" options. + ### Best Practices: - Use shadcn/radix components (Button, Input, Card, etc.) for consistent UI diff --git a/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/shared/types.ts b/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/shared/types.ts index 58308d05bd..a31ee4e47d 100644 --- a/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/shared/types.ts +++ b/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/shared/types.ts @@ -1,3 +1,13 @@ export interface QueryResult { value: string; } + +// SQL type helpers - numeric fields from Databricks SQL return as strings +export const toNumber = (val: string | number | null | undefined): number => + Number(val || 0); + +export const formatCurrency = (val: string | number | null | undefined): string => + `$${toNumber(val).toFixed(2)}`; + +export const formatPercent = (val: string | number | null | undefined): string => + `${toNumber(val).toFixed(1)}%`; diff --git a/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/tests/smoke.spec.ts b/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/tests/smoke.spec.ts index 3f3d1d6490..d712c69588 100644 --- a/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/tests/smoke.spec.ts +++ b/experimental/apps-mcp/templates/appkit/template/{{.project_name}}/tests/smoke.spec.ts @@ -12,10 +12,10 @@ test('smoke test - app loads and displays data', async ({ page }) => { // Navigate to the app await page.goto('/'); - // Wait for the page title to be visible + // ⚠️ UPDATE THESE SELECTORS after customizing App.tsx: + // - Change heading name to match your app title + // - Change data selector to match your primary data display await expect(page.getByRole('heading', { name: 'Minimal Databricks App' })).toBeVisible(); - - // Wait for SQL query result to load (wait for "hello world" to appear) await expect(page.getByText('hello world', { exact: true })).toBeVisible({ timeout: 30000 }); // Wait for health check to complete (wait for "OK" status)