Dashboard SDK

Dashboard SDK

The Dashboard SDK is a monorepo of separate npm packages that work together to build Huitzo Dashboards that run inside Hub. The core is framework-agnostic; React bindings are layered on top.

Packages

Package npm Name Provides
Core @huitzo/dashboard-sdk Auth client, API client, types, error classes. Framework-agnostic. No DOM, no framework imports.
React @huitzo/dashboard-sdk-react Hooks (useCommand, useHuitzo, useRealtime, useHubNavigation, etc.) and components (HuitzoProvider). Depends on core as peer.

Installation

# Core SDK (framework-agnostic)
npm install @huitzo/dashboard-sdk

# React hooks and components
npm install @huitzo/dashboard-sdk @huitzo/dashboard-sdk-react

Quick Start

Core Client (Framework-Agnostic)

The core package works in any JavaScript/TypeScript environment—Node, Deno, or browser—without React or any framework:

import { HuitzoClient } from '@huitzo/dashboard-sdk';

const huitzo = new HuitzoClient({
  apiUrl: 'https://huitzo.ai',  // Optional, defaults to current origin
});

// Execute a command
const result = await huitzo.commands.execute('@acme/claims/process-claim', {
  claimId: 'CLM-12345'
});

console.log(result);

For React dashboards running inside Hub, pass the context object from mount() to HuitzoProvider:

# pseudocode  conceptual React usage
import type { HuitzoMountContext } from '@huitzo/dashboard-sdk-react';
import { HuitzoProvider, useCommand } from '@huitzo/dashboard-sdk-react';

// 1. Dashboard entry point receives context from Hub's mount()
export function mount(container: HTMLElement, context: HuitzoMountContext) {
  const root = createRoot(container);
  root.render(
    <HuitzoProvider context={context}>
      <ClaimsList />
    </HuitzoProvider>
  );
}

// 2. Use hooks in your components
function ClaimsList() {
  const { execute, data, loading, error } = useCommand('@acme/claims/list-claims');

  useEffect(() => { execute({ status: 'pending' }) }, []);

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return <ul>{data?.claims.map(claim => <li key={claim.id}>{claim.title}</li>)}</ul>;
}

Core Client

The HuitzoClient is the foundation of the SDK. It handles authentication and API calls. The core package uses openapi-fetch for typed HTTP requests against the Huitzo REST API.

Initialization

# pseudocode
import { HuitzoClient } from '@huitzo/dashboard-sdk';

// Basic initialization (uses current origin)
const huitzo = new HuitzoClient();

// With options
const huitzo = new HuitzoClient({
  apiUrl: 'https://huitzo.ai',
  timeout: 60000,
  onAuthError: (error) => { /* redirect to login */ },
});

Configuration Options

Option Type Default Description
apiUrl string Current origin Huitzo API base URL
timeout number 60000 Request timeout (ms)
onAuthError function - Called when auth fails (receives Error)

Sub-Objects

Sub-object Class Description
huitzo.auth AuthApi Login, register, refresh, logout, API key
huitzo.commands CommandsApi Execute and list Intelligence Pack commands
huitzo.packs PacksApi List and manage packs
huitzo.account AccountApi Account mode checks, developer mode
huitzo.organizations OrganizationsApi Organization and member management
huitzo.health HealthApi Liveness, readiness, and integration probes

Authentication

The SDK handles authentication via email/password login, token refresh, or API key. When running inside Hub, the HuitzoProvider syncs the JWT from the mount context automatically.

# pseudocode
// Email/password login
await huitzo.auth.login('[email protected]', 'password');

// Register a new account
await huitzo.auth.register('[email protected]', 'password', 'acme', 'access_code');

// API key (for development)
huitzo.auth.setApiKey('sk_...');

// Check auth status
const isAuthenticated = huitzo.auth.isAuthenticated();
const user = await huitzo.auth.getUser();

// Logout
await huitzo.auth.logout();

Commands API

Execute Intelligence Pack commands from your dashboard.

Execute Command

// Basic execution
const result = await huitzo.commands.execute('@acme/claims/process-claim', {
  claimId: 'CLM-12345',
  priority: 'high'
});

// With options
const result = await huitzo.commands.execute(
  '@acme/claims/process-claim',
  { claimId: 'CLM-12345' },
  {
    timeout: 60000,      // Override timeout
    signal: controller.signal,  // AbortController signal
  }
);

Command Response

# pseudocode
interface CommandResult<T> {
  result: T;                    // The command's return value
  execution: {
    duration_ms: number;        // Server-side execution time
    status: string;             // e.g. "success"
  };
}

// Type-safe usage
interface ClaimResult {
  id: string;
  status: string;
  processedAt: string;
}

const result = await huitzo.commands.execute<ClaimResult>(
  '@acme/claims/process-claim',
  { claimId: 'CLM-12345' }
);

console.log(result.result.status);       // "pending" | "approved" | ...
console.log(result.execution.duration_ms);

CommandResult<T> is the unwrapped payload. The HTTP envelope fields (success, correlationId, timestamps) are consumed by the client and surfaced on errors via HuitzoError.correlationId, not on successful responses.

List Available Commands

# pseudocode
// List all commands accessible to the current tenant
const all = await huitzo.commands.list();

// Filter by pack namespace (client-side filter over the full list)
const claims = await huitzo.commands.list({ pack: 'claims' });

// Get details for one command
const command = await huitzo.commands.get('@acme/claims/process-claim');
console.log(command.description);
console.log(command.input_schema);

CommandInfo.namespace is the pack namespace (e.g. "claims"), and CommandInfo.name is the command name within that pack. The pack filter matches commands whose namespace is exactly equal to the value passed.

Check Pack Availability

Pack-availability checks are cached in React via the usePacks hook, which exposes an isInstalled(packId) helper backed by the provider's cached list:

# pseudocode
import { usePacks } from '@huitzo/dashboard-sdk-react';

function ClaimsFeature() {
  const { isInstalled, loading } = usePacks();
  if (loading) return null;
  if (!isInstalled('@acme/claims-processor')) return <UpsellBanner />;
  return <ClaimsPanel />;
}

In framework-agnostic code, query the list directly:

# pseudocode
const packs = await huitzo.packs.list();
const isInstalled = packs.some(p => p.id === '@acme/claims-processor');

Real-Time API

Status: Planned (not yet implemented). The huitzo.realtime sub-object does not exist in the current SDK. The hooks useRealtime and useConnectionStatus are exported as stubs that throw HuitzoError on invocation. This section describes the planned API.

Subscribe to real-time events via WebSocket.

Subscribe to Events (Planned)

# pseudocode  planned API
const unsubscribe = huitzo.realtime.subscribe('claims:updated', {
  onMessage: (event) => { console.log('Claim updated:', event.data) },
  onError: (error) => { console.error('Subscription error:', error) },
});

unsubscribe();

Event Types (Planned)

Event Description Payload
{pack}:created Item created { id, type, data }
{pack}:updated Item updated { id, type, changes }
{pack}:deleted Item deleted { id, type }
command:started Command execution started { correlationId, command }
command:completed Command execution completed { correlationId, result }
command:failed Command execution failed { correlationId, error }

Connection Management (Planned)

# pseudocode  planned API
const status = huitzo.realtime.getStatus();  // 'connected' | 'connecting' | 'disconnected'
await huitzo.realtime.connect();
huitzo.realtime.disconnect();
huitzo.realtime.on('connected', () => {});
huitzo.realtime.on('disconnected', () => {});

React Integration

The React integration provides hooks and components for building dashboards with React.

HuitzoProvider

Wrap your application with HuitzoProvider to enable hooks. When running inside Hub, pass the context object received from mount():

// Dashboard entry point (src/main.tsx)
import { createRoot } from 'react-dom/client';
import type { HuitzoContext } from '@huitzo/dashboard-sdk';
import { HuitzoProvider } from '@huitzo/dashboard-sdk-react';

export function mount(container: HTMLElement, context: HuitzoContext) {
  const root = createRoot(container);
  root.render(
    <HuitzoProvider context={context}>
      <Dashboard />
    </HuitzoProvider>
  );
}

Context Integration

When a context object is provided (passed from Hub's mount() call), the provider: - Uses Hub's shared JWT session via context.token - Enables Hub navigation hooks via context.navigate - Provides user info and dashboard metadata - Enables the Hub event bus for cross-boundary communication

See Dashboard Loading Architecture — HuitzoContext for the full context shape.

useHuitzo

Access the Huitzo client, auth state, mount context, and provider-level init state.

Returns: { client, user, isAuthenticated, initError, packs, mountContext, isPackInstalled }

Field Type Description
client HuitzoClient The client instance created by the provider.
user UserInfo \| null The currently authenticated user, or null.
isAuthenticated boolean Whether the client holds valid auth tokens.
initError Error \| null Error from the provider's initial data fetch (user or packs). 401s do not appear here — they trigger onAuthError instead.
packs PackInfo[] Cached list of installed packs from the provider's initial fetch.
mountContext HuitzoMountContext The vanilla JS context object Hub passed to mount(). Used by Hub-specific hooks.
isPackInstalled (packId: string) => boolean Membership check against the cached packs list.
# pseudocode
import { useHuitzo } from '@huitzo/dashboard-sdk-react';

function UserInfo() {
  const { client, user, isAuthenticated, initError, isPackInstalled } = useHuitzo();

  if (initError) return <ErrorBanner error={initError} />;
  if (!isAuthenticated) return <LoginButton />;

  return (
    <div>
      <p>Welcome, {user.email}</p>
      {isPackInstalled('@huitzo/analytics') && <AnalyticsWidget />}
    </div>
  );
}

useCommand

Execute commands with automatic state management:

import { useCommand } from '@huitzo/dashboard-sdk-react';

interface Claim {
  id: string;
  title: string;
  status: string;
}

interface ListClaimsResult {
  claims: Claim[];
  total: number;
}

function ClaimsList() {
  const {
    execute,
    data,
    loading,
    error,
    reset
  } = useCommand<ListClaimsResult>('@acme/claims/list-claims');

  // Execute on mount
  useEffect(() => {
    execute({ status: 'pending', limit: 20 });
  }, []);

  // Execute with different params
  const handleFilter = (status: string) => {
    execute({ status, limit: 20 });
  };

  if (loading) return <Skeleton count={5} />;
  if (error) return <ErrorMessage error={error} onRetry={() => execute()} />;

  return (
    <div>
      <FilterBar onFilter={handleFilter} />
      <ul>
        {data?.claims.map(claim => (
          <ClaimItem key={claim.id} claim={claim} />
        ))}
      </ul>
      <p>Total: {data?.total}</p>
    </div>
  );
}

useCommand Options

Returns: { execute, data, loading, error, reset, status, isIdle, isSuccess, isError }

# pseudocode
const {
  execute,
  data,
  loading,
  error,
  reset,
  status,     // 'idle' | 'loading' | 'success' | 'error'
  isIdle,
  isSuccess,
  isError
} = useCommand('@acme/claims/process', {
  // Execute immediately with these args
  initialArgs: { claimId: 'CLM-123' },

  // Callbacks
  onSuccess: (data) => { toast.success('Claim processed!') },
  onError: (error) => { toast.error(error.message) },

  // Cache result for 5 seconds (keyed by args)
  cacheTime: 5000,
});

useRealtime

Status: Stub — throws HuitzoError on invocation. Depends on client.realtime which is not yet implemented in the core SDK.

Will subscribe to real-time events with automatic cleanup on unmount. Planned signature:

# pseudocode  planned API
useRealtime('claims:updated', (event) => { /* handle event */ });
useRealtime(['claims:created', 'claims:deleted'], (event) => { /* handle */ });

useConnectionStatus

Status: Stub — throws HuitzoError on invocation. Depends on client.realtime which is not yet implemented in the core SDK.

Will monitor WebSocket connection. Planned return: { status, isConnected, reconnect }

usePacks

Query installed packs.

Returns: { packs, loading, error, isInstalled, refetch }

# pseudocode
import { usePacks } from '@huitzo/dashboard-sdk-react';

function PackList() {
  const { packs, loading, error, isInstalled, refetch } = usePacks();

  if (loading) return <p>Loading packs...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {packs.map(pack => (
        <li key={pack.name}>{pack.name} - {pack.version}</li>
      ))}
    </ul>
  );
}

Hub Integration

These hooks work when the dashboard runs inside Hub with a HuitzoContext provided via mount().

useHubNavigation

Navigate between Hub views.

Returns: { navigateToHub, navigateToDashboard, navigateToExplore, navigateToSettings, currentDashboard }

# pseudocode
import { useHubNavigation } from '@huitzo/dashboard-sdk-react';

function DashboardHeader() {
  const {
    navigateToHub,        // Go to Hub home
    navigateToDashboard,  // Go to specific dashboard
    navigateToExplore,    // Go to marketplace
    navigateToSettings,   // Go to settings
    currentDashboard,     // Current dashboard slug
  } = useHubNavigation();

  return (
    <header>
      <button onClick={navigateToHub}> Hub</button>
      <h1>Claims Dashboard</h1>
      <button onClick={() => navigateToDashboard('analytics')}>Open Analytics</button>
    </header>
  );
}

useHubContext

Access Hub environment context. Theme is reactive — updates when Hub toggles data-theme.

Returns: { apiUrl, dashboardSlug, user, theme }

# pseudocode
import { useHubContext } from '@huitzo/dashboard-sdk-react';

function MyComponent() {
  const {
    apiUrl,         // Huitzo API base URL
    dashboardSlug,  // Current dashboard slug
    user,           // Current user from Hub session
    theme,          // Hub's current theme ('light' | 'dark')
  } = useHubContext();

  return (
    <div className={theme}>
      <span>Dashboard: {dashboardSlug}</span>
      <span>User: {user?.email}</span>
    </div>
  );
}

useHubBreadcrumbs

Status: Stub — throws HuitzoError on invocation.

Will set breadcrumbs in Hub's header. Planned signature:

# pseudocode  planned API
useHubBreadcrumbs([
  { label: 'Claims Dashboard', href: '/d/claims-dashboard' },
  { label: 'Claim #123' },
]);

useHubActions

Status: Stub — throws HuitzoError on invocation.

Will trigger Hub-level UI actions (notifications, dialogs, settings). Planned return: { showNotification, showConfirmDialog, openSettings }


Components

The React package ships a small set of presentational components that match the Huitzo Hub design system. They are framework-agnostic in intent — they take typed props, accept either onClick or href for navigation, and apply design tokens from @huitzo/dashboard-sdk-react/styles/tokens.css.

Component Source Purpose
DashboardTile packages/dashboard-sdk-react/src/components/DashboardTile.tsx Clickable tile with icon, name, description, optional pinned indicator, and a custom accent color. Pass either onClick or href (mutually exclusive).
DashboardInfoBlock packages/dashboard-sdk-react/src/components/DashboardInfoBlock.tsx Card-like block displaying a title, description, optional icon, and optional metadata key-value pairs. Variant: default / elevated / outlined.

DashboardTile

# pseudocode  conceptual usage
import { DashboardTile } from '@huitzo/dashboard-sdk-react';

<DashboardTile
  name="Claims"
  description="Process and review insurance claims"
  icon="📋"
  isPinned
  color="var(--color-accent)"
  href="/d/claims"
/>

The href form is preferred over onClick when navigating within Hub, so the SDK can apply same-origin / https:// allowlisting (cross-origin and http:// URLs are blocked by default; see allowInsecureHttp for local dev).

DashboardInfoBlock

# pseudocode  conceptual usage
import { DashboardInfoBlock } from '@huitzo/dashboard-sdk-react';

<DashboardInfoBlock
  title="Claims Dashboard"
  description="Manage and process insurance claims"
  icon="📋"
  variant="elevated"
  metadata={[
    { label: 'Version', value: '1.2.0' },
    { label: 'Last updated', value: '2 hours ago' },
  ]}
/>

Theming

Both components consume CSS custom properties from @huitzo/dashboard-sdk-react/styles/tokens.css. Import that file once at the top of the dashboard entry point. Dark mode is the default; switch to light by setting data-theme="light" on a parent element.


Primitives

In addition to the React components above, the SDK ships a small set of theme-aware utility CSS classes for building dashboards that match the Hub visual language. They are delivered alongside the design tokens and are picked up automatically when the dashboard imports @huitzo/dashboard-sdk-react/styles.

Class names follow hz-{block}__{element}--{modifier} (BEM-ish). All colors resolve through Hub --color-* tokens, so the primitives flip between light and dark themes without any additional work. The terminal chrome uses fixed dark hex values intentionally — a terminal should look like a terminal regardless of the surrounding theme.

Family Purpose
.hz-card, .hz-card--lg, .hz-card--md Elevated card with shadow + border
.hz-card--accent, .hz-card--success, .hz-card--warning Card variants with accent border / top-border
.hz-stat__number, .hz-stat__number--success, .hz-stat__number--warning Iconic numeric display, clamp(3.75rem, 8vw, 6rem)
.hz-rail, .hz-step, .hz-step__badge Numbered onboarding rail with vertical gradient line
.hz-terminal, .hz-terminal__header, .hz-terminal__dots, .hz-terminal__dot, .hz-terminal__label Terminal block chrome (traffic-light dots + label)
.hz-terminal__body Terminal command line (light text on dark bg, monospace)
.hz-terminal__prompt Green $ prompt prefix; pair with .hz-terminal__body
.hz-terminal__output Expected output below a dashed divider
.hz-terminal__copy, .hz-terminal__copy--ok Copy button with success state
.hz-btn, .hz-btn--primary, .hz-btn--secondary, .hz-btn--ghost Standardized buttons
.hz-eyebrow, .hz-eyebrow--accent Uppercase section labels
.hz-kbd Inline keyboard chip
.hz-code Inline code

Example: numbered onboarding step + terminal block

# pseudocode  conceptual usage
<div className="hz-rail">
  <section className="hz-step hz-card hz-card--lg">
    <span className="hz-step__badge" aria-hidden>1</span>
    <h3>Run a pack</h3>
    <div className="hz-terminal">
      <div className="hz-terminal__header">
        <span className="hz-terminal__dots">
          <span className="hz-terminal__dot" />
          <span className="hz-terminal__dot" />
          <span className="hz-terminal__dot" />
        </span>
        <span className="hz-terminal__label">Terminal</span>
        <button className="hz-terminal__copy">Copy</button>
      </div>
      <pre className="hz-terminal__body">
        <span className="hz-terminal__prompt">$</span>
        huitzo run @yc/yc-weather-demo/current-weather --city san-francisco
      </pre>
      <pre className="hz-terminal__output">
         {`{"city": "san-francisco", "temp_c": 14.2}`}
      </pre>
    </div>
  </section>
</div>

When to use what

  • Use a DashboardTile / DashboardInfoBlock component when the unit is a discrete reusable thing (a tile in a grid, an info card with metadata).
  • Use hz-* primitives when you are composing a page-level layout (hero + onboarding rail + terminal block + CTA). They are layout-free by design — bring your own grid / flex.

The primitives intentionally do not impose display: flex or grid layouts — combine them with whatever layout system your dashboard uses (CSS grid, Tailwind utilities, flexbox).


Error Handling

Error Hierarchy

All SDK errors extend HuitzoError and carry structured metadata:

Class HTTP Status Error Code Description
AuthenticationError 401 AUTHENTICATION_FAILED Missing or invalid credentials
AuthorizationError 403 PERMISSION_DENIED Insufficient permissions
NotFoundError 404 NOT_FOUND Resource does not exist
ValidationError 400/422 VALIDATION_FAILED Input validation failure
TimeoutError 408 TIMEOUT Request timed out
RateLimitError 429 RATE_LIMITED Rate limit exceeded (retryAfter field)
CommandError varies COMMAND_ERROR Command execution failure
NetworkError - NETWORK_ERROR DNS failure, offline, CORS
InternalError 500 INTERNAL_ERROR Unexpected server error
IntegrationError 502 INTEGRATION_ERROR External integration failure
ServiceUnavailableError 503 SERVICE_UNAVAILABLE Temporary unavailability

Error Properties

Every HuitzoError instance carries:

Property Type Description
code ErrorCode Machine-readable error code
statusCode number \| undefined HTTP status (undefined for network errors)
details unknown Structured context (field errors, etc.)
correlationId string \| undefined Request ID for debugging
timestamp string \| undefined Server ISO 8601 timestamp
# pseudocode
import {
  HuitzoError,
  AuthenticationError,
  ValidationError,
  RateLimitError,
  NetworkError
} from '@huitzo/dashboard-sdk';

try {
  await huitzo.commands.execute('@acme/claims/process', args);
} catch (error) {
  if (error instanceof AuthenticationError) {
    // 401 — redirect to login
  } else if (error instanceof ValidationError) {
    // 400/422 — field errors in error.details
  } else if (error instanceof RateLimitError) {
    // 429 — retry after error.retryAfter seconds
  } else if (error instanceof NetworkError) {
    // DNS failure, offline, etc.
  } else if (error instanceof HuitzoError) {
    // Catch-all for any SDK error
    console.error(error.code, error.message);
  }
}

React Error Handling

# pseudocode
function ClaimProcessor() {
  const { execute, error, isError } = useCommand('@acme/claims/process');

  if (isError) {
    if (error instanceof ValidationError) {
      return <ValidationErrors errors={error.details} />;
    }
    if (error instanceof AuthorizationError) {
      return <PermissionDenied />;
    }
    return <GenericError error={error} />;
  }
}

TypeScript Support

The SDK is written in TypeScript and provides full type definitions.

Command Types

// Define your command types
interface ProcessClaimArgs {
  claimId: string;
  priority?: 'low' | 'medium' | 'high';
}

interface ProcessClaimResult {
  id: string;
  status: 'approved' | 'denied' | 'pending';
  processedAt: string;
  reviewer?: string;
}

// Use with execute
const result = await huitzo.commands.execute<ProcessClaimResult>(
  '@acme/claims/process',
  { claimId: 'CLM-123', priority: 'high' } satisfies ProcessClaimArgs
);

// result.result is typed as ProcessClaimResult

React Hook Types

# pseudocode
// useCommand is generic
const { data } = useCommand<ListClaimsResult>('@acme/claims/list');
// data is ListClaimsResult | null

Best Practices

1. Handle Loading States

function ClaimDetails({ id }: { id: string }) {
  const { execute, data, loading, error } = useCommand('@acme/claims/get');

  useEffect(() => {
    execute({ claimId: id });
  }, [id]);

  // Always handle all states
  if (loading) return <Skeleton />;
  if (error) return <ErrorDisplay error={error} />;
  if (!data) return null;

  return <ClaimCard claim={data} />;
}

2. Optimistic Updates

function ClaimActions({ claim }: { claim: Claim }) {
  const [optimisticStatus, setOptimisticStatus] = useState(claim.status);
  const { execute } = useCommand('@acme/claims/update-status');

  const handleApprove = async () => {
    setOptimisticStatus('approved');  // Optimistic update

    try {
      await execute({ claimId: claim.id, status: 'approved' });
    } catch (error) {
      setOptimisticStatus(claim.status);  // Revert on error
      toast.error('Failed to approve claim');
    }
  };

  return (
    <StatusBadge status={optimisticStatus} />
  );
}
function ClaimSearch() {
  const [query, setQuery] = useState('');
  const { execute, data, loading } = useCommand('@acme/claims/search');

  // Debounce search
  useEffect(() => {
    const timer = setTimeout(() => {
      if (query.length >= 2) {
        execute({ query });
      }
    }, 300);

    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {loading ? <Spinner /> : <Results data={data} />}
    </div>
  );
}

4. Clean Up Subscriptions (Planned — Real-Time API)

Once useRealtime is implemented, it will automatically clean up on unmount:

# pseudocode  planned API
useRealtime('claims:created', (event) => {
  setClaims(prev => [event.data, ...prev]);
});