From 84f8740d0907e5c4dc356eca3f2b28e312741579 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:29:25 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20honest=20implementation=20=E2=80=94?= =?UTF-8?q?=20DB=20persistence=20for=20aquaculture/insurance,=20real=20aut?= =?UTF-8?q?h=20router,=20Keycloak=20realm=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STREAM 1 (DB Persistence): - Aquaculture AI router: persist all predictions/diagnoses to PostgreSQL - Aquaculture feed router: persist feed records via requireDb() - Crop insurance service: replace BoundedMap with PostgreSQL queries - Add 50+ Drizzle schema tables for all missing features STREAM 2 (Real Auth): - Create auth-router with register/login/refresh/logout (all DB-backed) - Remove demo user fallback from trpc-base.ts - Remove dev-only JWT_SECRET fallback (require env var) - Create Keycloak farmconnect realm config (RS256, PKCE, 7 roles) - Update keycloak.ts to use farmconnect realm STREAM 3 (UI): - Add SmartAlex unified CSS design tokens (.sa-hero, .sa-card, .sa-btn-primary) - Create SmartAlexDesignSystem.tsx shared component library Build: tsc 0 errors, 586 tests pass Co-Authored-By: Patrick Munis --- .../components/ui/SmartAlexDesignSystem.tsx | 561 ++++++++++++++++++ client/src/index.css | 106 ++++ drizzle/schema-honest-implementation.ts | 542 +++++++++++++++++ k8s/keycloak/farmconnect-realm.json | 102 ++++ server/_core/trpc-base.ts | 66 +-- server/db.ts | 5 +- server/keycloak.ts | 4 +- server/routers/aquaculture-ai-router.ts | 94 ++- server/routers/aquaculture-feed-router.ts | 36 +- server/routers/auth-router.ts | 221 +++++++ server/services/crop-insurance-service.ts | 85 ++- server/trpc.ts | 5 +- 12 files changed, 1708 insertions(+), 119 deletions(-) create mode 100644 client/src/components/ui/SmartAlexDesignSystem.tsx create mode 100644 drizzle/schema-honest-implementation.ts create mode 100644 k8s/keycloak/farmconnect-realm.json create mode 100644 server/routers/auth-router.ts diff --git a/client/src/components/ui/SmartAlexDesignSystem.tsx b/client/src/components/ui/SmartAlexDesignSystem.tsx new file mode 100644 index 00000000..258f74ea --- /dev/null +++ b/client/src/components/ui/SmartAlexDesignSystem.tsx @@ -0,0 +1,561 @@ +/** + * SmartAlex Design System + * + * Shared components implementing the SmartAlex visual language: + * - Teal gradient heroes and buttons + * - Consistent KPI cards with trend indicators + * - Unified page headers with icons + * - Data tables with consistent styling + * - Form dialogs with standard layout + * - Empty states with illustrations + */ +import React, { ReactNode, useState } from 'react'; + +// ============================================================================ +// DESIGN TOKENS +// ============================================================================ + +export const tokens = { + colors: { + primary: '#0d9488', // teal-600 + primaryDark: '#0f766e', // teal-700 + primaryLight: '#14b8a6', // teal-500 + accent: '#f59e0b', // amber-500 + success: '#10b981', // emerald-500 + danger: '#ef4444', // red-500 + warning: '#f59e0b', // amber-500 + info: '#3b82f6', // blue-500 + muted: '#6b7280', // gray-500 + background: '#f9fafb', // gray-50 + surface: '#ffffff', + text: '#111827', // gray-900 + textSecondary: '#6b7280', // gray-500 + }, + gradients: { + hero: 'linear-gradient(135deg, #0d9488 0%, #14b8a6 50%, #2dd4bf 100%)', + button: 'linear-gradient(135deg, #0d9488 0%, #0f766e 100%)', + accent: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', + card: 'linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%)', + }, +} as const; + +// ============================================================================ +// PAGE HEADER (replaces inconsistent h1/h2 patterns across 157 pages) +// ============================================================================ + +interface PageHeaderProps { + icon: ReactNode; + title: string; + subtitle?: string; + actions?: ReactNode; + variant?: 'hero' | 'compact'; +} + +export function PageHeader({ icon, title, subtitle, actions, variant = 'compact' }: PageHeaderProps) { + if (variant === 'hero') { + return ( +
+
+
+
+ {icon} +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ {actions &&
{actions}
} +
+
+ ); + } + + return ( +
+
+
+ {icon} +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ {actions &&
{actions}
} +
+ ); +} + +// ============================================================================ +// KPI CARD (replaces 50+ different stat card implementations) +// ============================================================================ + +interface KpiCardProps { + label: string; + value: string | number; + icon?: ReactNode; + trend?: { value: number; label?: string }; + target?: string; + color?: 'teal' | 'amber' | 'emerald' | 'red' | 'blue' | 'purple'; +} + +const kpiColorMap = { + teal: 'bg-teal-50 text-teal-600', + amber: 'bg-amber-50 text-amber-600', + emerald: 'bg-emerald-50 text-emerald-600', + red: 'bg-red-50 text-red-600', + blue: 'bg-blue-50 text-blue-600', + purple: 'bg-purple-50 text-purple-600', +}; + +export function KpiCard({ label, value, icon, trend, target, color = 'teal' }: KpiCardProps) { + return ( +
+
+
+

{label}

+

{value}

+ {target &&

Target: {target}

} +
+ {icon && ( +
+ {icon} +
+ )} +
+ {trend && ( +
+ = 0 ? 'text-emerald-600' : 'text-red-600'}`}> + {trend.value >= 0 ? '↑' : '↓'} {Math.abs(trend.value)}% + + {trend.label && {trend.label}} +
+ )} +
+ ); +} + +// ============================================================================ +// ACTION BUTTON (replaces inconsistent button patterns) +// ============================================================================ + +interface ActionButtonProps { + children: ReactNode; + onClick?: () => void; + variant?: 'primary' | 'secondary' | 'outline' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + icon?: ReactNode; + disabled?: boolean; + type?: 'button' | 'submit'; +} + +export function ActionButton({ children, onClick, variant = 'primary', size = 'md', icon, disabled, type = 'button' }: ActionButtonProps) { + const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2'; + + const sizeClasses = { + sm: 'px-3 py-1.5 text-sm gap-1.5', + md: 'px-4 py-2.5 text-sm gap-2', + lg: 'px-6 py-3 text-base gap-2', + }; + + const variantClasses = { + primary: 'sa-btn-primary text-white focus:ring-teal-500 disabled:opacity-50', + secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-500', + outline: 'border-2 border-teal-600 text-teal-600 hover:bg-teal-50 focus:ring-teal-500', + danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', + }; + + return ( + + ); +} + +// ============================================================================ +// DATA TABLE (replaces 50+ different table patterns) +// ============================================================================ + +interface Column { + key: string; + header: string; + render?: (item: T) => ReactNode; + align?: 'left' | 'center' | 'right'; +} + +interface DataTableProps { + columns: Column[]; + data: T[]; + emptyMessage?: string; + emptyIcon?: ReactNode; + isLoading?: boolean; +} + +export function DataTable>({ columns, data, emptyMessage = 'No records found.', emptyIcon, isLoading }: DataTableProps) { + if (isLoading) { + return ( +
+
+

Loading...

+
+ ); + } + + return ( +
+
+ + + + {columns.map((col) => ( + + ))} + + + + {data.length === 0 ? ( + + + + ) : ( + data.map((item, idx) => ( + + {columns.map((col) => ( + + ))} + + )) + )} + +
+ {col.header} +
+ {emptyIcon &&
{emptyIcon}
} +

{emptyMessage}

+
+ {col.render ? col.render(item) : String(item[col.key] ?? '—')} +
+
+
+ ); +} + +// ============================================================================ +// PERIOD SELECTOR (replaces many date range selectors) +// ============================================================================ + +interface PeriodSelectorProps { + periods: { key: string; label: string }[]; + selected: string; + onSelect: (key: string) => void; +} + +export function PeriodSelector({ periods, selected, onSelect }: PeriodSelectorProps) { + return ( +
+ {periods.map((p) => ( + + ))} +
+ ); +} + +// ============================================================================ +// STATUS BADGE (replaces many status rendering patterns) +// ============================================================================ + +interface StatusBadgeProps { + status: string; + variant?: 'dot' | 'pill'; +} + +const statusColors: Record = { + active: 'bg-emerald-100 text-emerald-700', + completed: 'bg-emerald-100 text-emerald-700', + approved: 'bg-emerald-100 text-emerald-700', + delivered: 'bg-emerald-100 text-emerald-700', + paid: 'bg-emerald-100 text-emerald-700', + pending: 'bg-amber-100 text-amber-700', + draft: 'bg-gray-100 text-gray-700', + review: 'bg-blue-100 text-blue-700', + in_transit: 'bg-blue-100 text-blue-700', + processing: 'bg-blue-100 text-blue-700', + failed: 'bg-red-100 text-red-700', + rejected: 'bg-red-100 text-red-700', + cancelled: 'bg-red-100 text-red-700', + defaulted: 'bg-red-100 text-red-700', + expired: 'bg-gray-100 text-gray-500', +}; + +export function StatusBadge({ status, variant = 'pill' }: StatusBadgeProps) { + const colorClass = statusColors[status.toLowerCase()] || 'bg-gray-100 text-gray-700'; + const label = status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); + + if (variant === 'dot') { + const dotColor = colorClass.includes('emerald') ? 'bg-emerald-500' : colorClass.includes('amber') ? 'bg-amber-500' : colorClass.includes('red') ? 'bg-red-500' : colorClass.includes('blue') ? 'bg-blue-500' : 'bg-gray-400'; + return ( + + + {label} + + ); + } + + return ( + + {label} + + ); +} + +// ============================================================================ +// DIALOG (replaces many inconsistent modal implementations) +// ============================================================================ + +interface DialogProps { + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; + actions?: ReactNode; + size?: 'sm' | 'md' | 'lg'; +} + +export function Dialog({ open, onClose, title, children, actions, size = 'md' }: DialogProps) { + if (!open) return null; + + const sizeClasses = { + sm: 'max-w-md', + md: 'max-w-lg', + lg: 'max-w-2xl', + }; + + return ( +
+
+
+
+
+

{title}

+ +
+
{children}
+ {actions && ( +
+ {actions} +
+ )} +
+
+
+ ); +} + +// ============================================================================ +// FORM FIELD (replaces many inconsistent input patterns) +// ============================================================================ + +interface FormFieldProps { + label: string; + name: string; + type?: 'text' | 'number' | 'email' | 'date' | 'textarea' | 'select'; + value: string | number; + onChange: (value: string) => void; + placeholder?: string; + required?: boolean; + options?: { value: string; label: string }[]; + error?: string; +} + +export function FormField({ label, name, type = 'text', value, onChange, placeholder, required, options, error }: FormFieldProps) { + const inputClasses = `w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent transition-all ${error ? 'border-red-300' : 'border-gray-300'}`; + + return ( +
+ + {type === 'select' && options ? ( + + ) : type === 'textarea' ? ( +