Skip to main content

Code Organisation

Keep your root component thin. src/ui/index.tsx should delegate immediately to a router or page component — it should not contain business logic. As your plugin grows, use this structure:
src/
├── ui/
│   └── index.tsx              ← root component (thin, delegates to pages)
├── pages/
│   ├── OverviewPage.tsx
│   └── DetailPage.tsx
├── components/
│   ├── PilotCard.tsx
│   └── StatsGrid.tsx
├── api/
│   └── index.ts               ← TanStack Query hooks for your API calls
└── background/
    └── index.ts               ← background module (if needed)
One default export per UI module. Your src/ui/index.tsx must have a single export default. Named exports are ignored by the shell loader.
// Correct
export default function MyPlugin() { ... }

// Wrong — shell won't find your component
export function MyPlugin() { ... }

Performance

Always use select with useSimData()

Simulator data updates at ~60fps. Without select, your component re-renders on every frame:
// Bad — re-renders ~60 times per second
const { data } = useSimData();
const altitude = data?.data?.altitude ?? 0;

// Good — re-renders only when altitude changes
const { data: altitude } = useSimData({
  select: (s) => s?.data?.altitude ?? 0,
});

Split components by data needs

If one part of your UI needs high-frequency sim data and another only needs the flight phase, make them separate components:
// AltitudeDisplay re-renders when altitude changes (~60fps)
function AltitudeDisplay() {
  const { data: alt } = useSimData({ select: (s) => s?.data?.altitude ?? 0 });
  return <span>{Math.round(alt).toLocaleString()} ft</span>;
}

// PhaseDisplay re-renders only on phase transitions
function PhaseDisplay() {
  const { data } = useFlightPhase();
  return <Badge>{data?.currentPhase ?? "---"}</Badge>;
}

// Parent rarely re-renders
export default function FlightPanel() {
  return (
    <div>
      <AltitudeDisplay />
      <PhaseDisplay />
    </div>
  );
}

Don’t poll when you have Socket.io

The SDK hooks (useSimData, useFlightPhase, useFlightEvents, useLandingAnalysis, useTrackingSession) all use Socket.io for real-time push updates. They hydrate once via REST on mount, then stay current via WebSocket. Don’t set up your own polling intervals for data the shell already pushes.

Data Fetching

Use TanStack Query for your own API calls. TanStack Query is provided by the shell at runtime:
import { useQuery } from "@tanstack/react-query";

function useFleetData() {
  return useQuery({
    queryKey: ["fleet"],
    queryFn: async () => {
      const res = await fetch("http://127.0.0.1:2066/api/fleet");
      const json = await res.json();
      return json.data;
    },
  });
}
Use query key factories to keep cache keys organised and enable targeted invalidation:
const fleetKeys = {
  all: ["fleet"] as const,
  list: () => [...fleetKeys.all, "list"] as const,
  detail: (id: string) => [...fleetKeys.all, "detail", id] as const,
};

function useFleetList() {
  return useQuery({
    queryKey: fleetKeys.list(),
    queryFn: fetchFleetList,
  });
}

function useFleetDetail(id: string) {
  return useQuery({
    queryKey: fleetKeys.detail(id),
    queryFn: () => fetchFleetDetail(id),
  });
}

Settings Design

Airline-scoped for VA-controlled content. Welcome messages, links, branding, event rules, route configurations — anything the VA admin should control centrally. These can be updated from the Skyvex platform without rebuilding the plugin. User-scoped for personal preferences. Unit systems, display toggles, notification preferences, refresh intervals. Each pilot can customise these independently. Always provide sensible defaults. Your plugin should work out of the box without any settings configured:
const interval = ctx.config.get<number>("refresh_interval", 30);
const showMap = ctx.config.get<boolean>("show_map", true);
const links = ctx.config.get<QuickLink[]>("quick_links", []);
Never assume a setting has a value.

Logging and Error Handling

Use the SDK logger

Never use console.log directly. Use the SDK loggers — they write to Stratos’ log files with timestamps and your plugin ID prefix, which is essential for troubleshooting production issues.
// In UI components
import { usePluginLogger } from "@skyvexsoftware/stratos-sdk";

function MyComponent() {
  const logger = usePluginLogger();
  logger.info("MyComponent", "User opened the fleet page");
  logger.error("MyComponent", "Failed to load fleet data", error);
}
// In background module
export async function onStart(ctx: PluginContext) {
  ctx.logger.info("Init", "Plugin starting...");
}
Always provide a category as the first argument. Use component names or functional areas — this makes log filtering much easier.

Handle async errors gracefully

For API calls and async operations, show meaningful error states:
const { data, isLoading, error } = useQuery({ ... });

if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage message="Failed to load fleet data" />;
return <FleetList data={data} />;

Use toast for transient errors

const toast = useShellToast();

try {
  await submitPirep(data);
  toast.success("PIREP submitted");
} catch (err) {
  toast.error("Failed to submit PIREP. Please try again.");
}
The shell wraps your plugin in an error boundary automatically. If your component throws during render, pilots see a friendly error screen with a retry button — not a white screen. You don’t need to add your own top-level error boundary.

Styling

Use Tailwind utility classes. Your plugin inherits the shell’s full Tailwind config including the theme system. Dark mode works automatically when you use semantic tokens. Use SDK UI components for consistency. Button, Card, Badge, Dialog, Input, etc. are all themed and support light/dark mode. Using a third-party component library will look out of place. Respect the layout. Your plugin renders in the main content area next to the sidebar. Fill the container and handle overflow:
export default function MyPlugin() {
  return (
    <div className="flex h-full flex-col overflow-y-auto p-6">
      {/* your content */}
    </div>
  );
}

Versioning

Follow semver. Bump the version in plugin.json for each release:
  • Patch (1.0.1): Bug fixes, minor text changes
  • Minor (1.1.0): New features, new settings
  • Major (2.0.0): Breaking changes, major redesigns
Keep plugin.json and package.json versions in sync.

Common Pitfalls

Bundling shared dependencies

If your bundle is unexpectedly large, you might be accidentally bundling React, TanStack, or the SDK. The createPluginConfig() Vite factory externalises these automatically — only if you’re using it. Check that your vite.config.ts uses createPluginConfig and not a custom Vite config.

Forgetting the default export

The UI module must have a default export that is a React component. Named exports are ignored:
// Correct
export default function MyPlugin() { ... }

// Wrong — shell won't find your component
export function MyPlugin() { ... }

Using console.log

console.log output goes to Electron DevTools but not to log files. Use the SDK logger so that logs are captured in production for troubleshooting.

Mutating sim data

The data returned from useSimData() is shared across all consumers via the TanStack Query cache. Never mutate it directly — treat it as read-only.

Not handling the “no flight active” state

Many hooks return null or empty data when no flight is being tracked. Always handle this case:
const { isTracking, currentFlight } = useTrackingSession();
if (!isTracking) return <NoFlightMessage />;

Background module not loading

If your background module isn’t running, check:
  1. Your vite.config.ts has background: { entry: "src/background/index.ts" }
  2. Your build script runs both builds: "build": "vite build && BUILD_TARGET=background vite build"
  3. dist/background/index.js exists after building
  4. Your module exports onStart and onStop (or uses createPlugin)

Stale plugin after changes

In dev mode, UI changes hot reload automatically. For other change types:
  • plugin.json — restart the app
  • Background module — rebuild (pnpm build) and restart Stratos
  • New npm dependencies — run pnpm install then restart

Plugin not appearing in sidebar

Check that:
  1. Your plugin.json is valid (use the $schema for editor validation)
  2. The id field uses only lowercase alphanumeric characters and hyphens
  3. Your plugin is installed in the Stratos plugins directory, or your dev server is running and connected
  4. No other plugin uses the same id

Debugging

UI: Open Chrome DevTools (Cmd+Option+I on macOS, Ctrl+Shift+I on Windows). Your plugin components appear in the React DevTools tree under the PluginHost wrapper. Background: Background module logs go to the Stratos log files:
  • macOS: ~/Library/Application Support/stratos/logs/stratos-YYYY-MM-DD.log
  • Windows: %APPDATA%\stratos\logs\stratos-YYYY-MM-DD.log
Network: Your plugin’s Express routes are available at http://127.0.0.1:2066/api/.... Test them directly with curl or a browser during development.