# zrok console with myzrok-extension: local setup

How to run the zrok web console locally with `@netfoundry/myzrok-extension` (Billing and Custom Domains pages),
against the hosted staging zrok API.

## What is required

| Requirement | Where |
| --- | --- |
| Code changes in `zrok/ui` (until they are merged into zrok) | [Code changes in zrok/ui](#code-changes-in-zrokui) |
| The extension built and linked into `zrok/ui/node_modules` | See `MYZROK_EXTENSION_BUILD.md` |
| A settings file, `zrok/ui/.env.local` | Below |
| `myzrok-orchestration` running on port 8106 | Its own repository |
| A zrok account on staging | `https://api-v2.staging.zrok.io` |

## Code changes in zrok/ui

These changes let the console load extensions from `.env.local`. They are not in zrok yet, so each team member applies
them to their local zrok checkout. They contain no NetFoundry package names.

| File | Change | Summary |
| --- | --- | --- |
| `ui/vite-plugin-extensions.ts` | **Created** | Reads `ZROK_UI_EXTENSIONS`, loads each listed package, and generates the `virtual:zrok-extensions` list |
| `ui/src/extensions/virtual.d.ts` | **Created** | Type declaration for `virtual:zrok-extensions` |
| `ui/vite.config.ts` | **Modified** | Reads the four `ZROK_UI_*` settings; sets the dev proxy, allowed hosts and extension scripts from them |
| `ui/src/extensions.config.ts` | **Modified** | Registers every extension from `virtual:zrok-extensions` |

### Apply with the patch file

The patch `zrok-ui-extensions.patch` contains all four changes. It applies to the `v2.0_ui_extensions` branch
(commit `dd0995c5`). From the zrok repository root:

```bash
git apply --check zrok-ui-extensions.patch   # confirm it applies
git apply zrok-ui-extensions.patch
```

Or create and edit the files by hand, as shown below.

### `ui/vite-plugin-extensions.ts` (created)

```ts
/**
 * Vite Plugin: Extensions
 *
 * Selects which extension packages are bundled into the UI at build time. The
 * list comes from the ZROK_UI_EXTENSIONS environment variable (comma-separated
 * npm package names); unset or empty means no extensions, which is the default
 * build.
 *
 * The selected packages are exposed to the app as the `virtual:zrok-extensions`
 * module, whose default export is the array of extension manifests. They are
 * registered in src/extensions.config.ts.
 *
 * Usage:
 *
 * ```bash
 * npm install --no-save @myorg/zrok-my-extension@1.0.0
 * ZROK_UI_EXTENSIONS=@myorg/zrok-my-extension npm run build
 * ```
 */

import type { Plugin } from 'vite';
import type { ExtensionManifestWithScripts } from './vite-plugin-extension-scripts';

const VIRTUAL_MODULE_ID = 'virtual:zrok-extensions';
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;

/**
 * Split a comma-separated list, dropping blanks.
 */
export function parseList(value: string | undefined): string[] {
  return (value ?? '')
    .split(',')
    .map(s => s.trim())
    .filter(Boolean);
}

/**
 * Import each extension package's manifest, so build-time features (such as
 * script injection) can read it. Fails the build if a package is missing.
 */
export async function loadExtensionManifests(
  packages: string[]
): Promise<ExtensionManifestWithScripts[]> {
  return Promise.all(
    packages.map(async pkg => {
      let manifest: ExtensionManifestWithScripts | undefined;
      try {
        manifest = (await import(pkg)).default;
      } catch (err) {
        throw new Error(
          `unable to load extension '${pkg}' listed in ZROK_UI_EXTENSIONS (is it installed?): ${err}`
        );
      }
      if (!manifest || typeof manifest.id !== 'string') {
        throw new Error(`extension '${pkg}' has no default export with an 'id'`);
      }
      return manifest;
    })
  );
}

/**
 * Vite plugin that generates the `virtual:zrok-extensions` module.
 */
export function extensionsPlugin(packages: string[]): Plugin {
  return {
    name: 'vite-plugin-zrok-extensions',

    resolveId(id: string) {
      if (id === VIRTUAL_MODULE_ID) {
        return RESOLVED_VIRTUAL_MODULE_ID;
      }
    },

    load(id: string) {
      if (id !== RESOLVED_VIRTUAL_MODULE_ID) {
        return;
      }
      const imports = packages.map((pkg, i) => `import ext${i} from ${JSON.stringify(pkg)};`);
      const names = packages.map((_, i) => `ext${i}`);
      return `${imports.join('\n')}\nexport default [${names.join(', ')}];\n`;
    },
  };
}
```

### `ui/src/extensions/virtual.d.ts` (created)

```ts
/**
 * Type declaration for the build-time generated extension list
 * (see vite-plugin-extensions.ts).
 */
declare module 'virtual:zrok-extensions' {
  const extensions: import('./types').ExtensionManifest[];
  export default extensions;
}
```

### `ui/vite.config.ts` (modified, full file)

```ts
import { defineConfig, loadEnv, type ProxyOptions } from 'vite'
import react from '@vitejs/plugin-react'
import { extensionScriptsPlugin } from './vite-plugin-extension-scripts'
import { extensionsPlugin, loadExtensionManifests, parseList } from './vite-plugin-extensions'

// ==============================================================
// Build/dev settings, read from the environment or a git-ignored
// ui/.env.local file (all optional):
//
//   ZROK_UI_EXTENSIONS     comma-separated extension packages to bundle
//                          (must be installed in node_modules)
//   ZROK_UI_API_TARGET     dev server target for /api/v2
//                          (default http://localhost:18080)
//   ZROK_UI_DEV_PROXY      extra dev proxy rules, comma-separated
//                          path=target pairs, e.g.
//                          /my-backend=http://localhost:8106
//   ZROK_UI_ALLOWED_HOSTS  extra dev server allowed hosts, comma-separated
// ==============================================================

function devProxy(env: Record<string, string>): Record<string, ProxyOptions> {
  const proxy: Record<string, ProxyOptions> = {
    '/api/v2': {
      target: env.ZROK_UI_API_TARGET || 'http://localhost:18080',
      changeOrigin: true,
    },
  };
  for (const rule of parseList(env.ZROK_UI_DEV_PROXY)) {
    // split at the first '=' only, so targets may contain '='
    const eq = rule.indexOf('=');
    const path = rule.slice(0, eq).trim();
    const target = rule.slice(eq + 1).trim();
    if (eq < 0 || !path || !target) {
      throw new Error(`invalid ZROK_UI_DEV_PROXY rule '${rule}', expected path=target`);
    }
    proxy[path] = { target, changeOrigin: true };
  }
  return proxy;
}

// https://vite.dev/config/
export default defineConfig(async ({ mode }) => {
  const env = loadEnv(mode, '.', 'ZROK_UI_');
  const extensionPackages = parseList(env.ZROK_UI_EXTENSIONS);
  const extensions = await loadExtensionManifests(extensionPackages);

  return {
    plugins: [
      react(),
      extensionsPlugin(extensionPackages),
      extensionScriptsPlugin({
        extensions,
        verbose: extensions.length > 0,
      }),
    ],
    build: {
      rollupOptions: {
        output: {
          manualChunks(id: string) {
            if (id.includes("node_modules")) {
              if (id.includes("react-dom") || id.includes("react-router") || id.includes("@emotion") || id.match(/\/react\//)) {
                return "vendor-react";
              }
              if (id.includes("@mui/x-charts") || id.includes("@mui/x-charts-vendor")) {
                return "vendor-mui-charts";
              }
              if (id.includes("@mui/x-date-pickers")) {
                return "vendor-mui-pickers";
              }
              if (id.includes("@mui")) {
                return "vendor-mui";
              }
              if (id.includes("@xyflow") || id.includes("d3-hierarchy")) {
                return "vendor-visualizer";
              }
              if (id.includes("material-react-table")) {
                return "vendor-table";
              }
            }
          },
        },
      },
    },
    server: {
      proxy: devProxy(env),
      allowedHosts: [
        ".share.zrok.io",
        ...parseList(env.ZROK_UI_ALLOWED_HOSTS),
      ],
    },
  }
})
```

### `ui/src/extensions.config.ts` (modified)

Add this import below the existing `extensionRegistry` import:

```ts
import buildExtensions from 'virtual:zrok-extensions';
```

Add this line inside `loadExtensions()`, before the closing comment block:

```ts
  // Extensions selected at build time via ZROK_UI_EXTENSIONS (see vite-plugin-extensions.ts)
  buildExtensions.forEach(ext => extensionRegistry.register(ext));
```

Remove the commented `VITE_ENABLE_BILLING` example, and any hard-coded `myzrokExtension` import or `register` line.

## Settings file: `zrok/ui/.env.local`

Create this file with the following content. It is git-ignored and never committed.

```bash
ZROK_UI_EXTENSIONS=@netfoundry/myzrok-extension
ZROK_UI_API_TARGET=https://api-v2.staging.zrok.io
ZROK_UI_DEV_PROXY=/myzrok-orchestration=http://localhost:8106
ZROK_UI_ALLOWED_HOSTS=.share.staging.zrok.io
```

| Setting | Required? | What it does |
| --- | --- | --- |
| `ZROK_UI_EXTENSIONS` | Yes | Loads the extension into the console. Must match the linked package name |
| `ZROK_UI_API_TARGET` | Yes | Sends console API calls (`/api/v2`) to the staging zrok API |
| `ZROK_UI_DEV_PROXY` | Yes, for Billing data | Sends Billing calls (`/myzrok-orchestration`) to your local orchestration |
| `ZROK_UI_ALLOWED_HOSTS` | No | Lets the local console accept staging share host names |

If orchestration runs on another port, change only `ZROK_UI_DEV_PROXY`.

## Files: what changes locally

| File or item | Change |
| --- | --- |
| `zrok/ui/.env.local` | **Create** it with the settings above |
| `zrok/ui/node_modules/@netfoundry/myzrok-extension` | **Link** to the extension folder (see `MYZROK_EXTENSION_BUILD.md`) |
| `zrok/ui/package.json`, `zrok/ui/package-lock.json` | **No change.** Do not add the extension here |
| `zrok/ui/src/extensions.config.ts`, `zrok/ui/vite.config.ts` | **Use the versions from [Code changes in zrok/ui](#code-changes-in-zrokui).** Do not hard-code the extension or proxy targets; they read `.env.local` |
| `myzrok-orchestration-develop/src/main/resources/application.properties` | **No change.** It already uses port 8106 and path `/myzrok-orchestration/v1` |

## Run it

1. Start orchestration in its own terminal:

    ```bash
    cd ~/git/myzrok-orchestration-develop
    ./run.sh
    ```

2. Start the console:

    ```bash
    cd ~/git/zrok/ui
    npm run dev
    ```

3. Open `http://localhost:5173` and log in with your staging account. Billing and Custom Domains appear in the nav.

Restart `npm run dev` after editing `.env.local` or rebuilding the extension.

## How local requests are routed

The local console server (`npm run dev`) forwards each request by its path:

| Request path | Goes to |
| --- | --- |
| `/api/v2/*` | Staging zrok API (`ZROK_UI_API_TARGET`) |
| `/myzrok-orchestration/*` | Local orchestration on port 8106 (`ZROK_UI_DEV_PROXY`) |
| Everything else | The console pages, including Billing |

## Common problems

| Symptom | Fix |
| --- | --- |
| `unable to load extension '@netfoundry/myzrok-extension' ... (is it installed?)` | The link is missing, usually after `npm install`. Link it again (see `MYZROK_EXTENSION_BUILD.md`) |
| Billing is not in the nav | Check `ZROK_UI_EXTENSIONS` in `.env.local`, then restart `npm run dev` |
| Billing page shows no data | Check that orchestration is running on 8106 and `ZROK_UI_DEV_PROXY` is set |
| `invalid ZROK_UI_DEV_PROXY rule` | Use the form `path=target`, e.g. `/myzrok-orchestration=http://localhost:8106` |
| Login fails | Use a staging account; `ZROK_UI_API_TARGET` must point at staging |
| Need the console without Billing | Run `ZROK_UI_EXTENSIONS= npm run dev` |
