# ProxyHosts

> A reverse-proxy host editor - a table of hosts with upstream, TLS mode and websockets, an enabled switch per host, and a dialog to add or edit one. Domains reuse DomainChips.

Source: https://docs.nasaqui.com/components/proxy-hosts

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/proxy-hosts.json
```

List and edit reverse-proxy hosts. Each row shows the domains it serves (with a "+N" chip when there are many), the upstream URL, the TLS mode (off, automatic, custom, passthrough), whether websockets are on, its status and an enabled switch. Add and edit share one dialog with validation for the domains and the upstream; delete asks first. Passthrough TLS hands the connection over untouched, so websockets are turned off. It has no backend: your callbacks save and you pass the updated `hosts` back.

## When to use

- A hosting panel or self-hosted proxy admin.
- Routing several domains to internal apps.

## When not to use

- Load balancing pools or health checks per upstream.
- Editing DNS: use `DnsManagement`.

## Import

```tsx
import { ProxyHosts } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { ProxyHosts, type ProxyHost } from "@fadymondy/nasaq/web";

declare const hosts: ProxyHost[];
declare const api: { save(input: unknown, id?: string): Promise<void>; remove(id: string): Promise<void> };

export const Proxy = () => (
  <ProxyHosts hosts={hosts} onSave={(input, id) => api.save(input, id)} onDelete={(id) => api.remove(id)} />
);
```

## Anatomy

```
ProxyHosts                   data-slot="proxy-hosts"
├─ toolbar                   search, Add proxy host
├─ DataTable                 domains (DomainChips), upstream, TLS Badge, websockets, Status, enabled Switch; row menu: Edit, Delete
├─ host Dialog               domains Textarea, upstream Input, TLS Select, websockets and enabled Switches
└─ AlertDialog               delete confirm
```

## API

**ProxyHosts**: every `div` prop except `children` and `onToggle`, plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hosts` | `readonly ProxyHost[]` | required | `{ id, hosts, upstream, tlsMode, websockets, enabled, status? }`. TLS: `off`, `auto`, `custom`, `passthrough`. Status: `online`, `offline`, `unknown`. |
| `loading` | `boolean` | `false` | Skeleton state. |
| `onSave` | `(input: ProxyHostInput, id?) => Promise<void \| { error? }>` | required | Create (no `id`) or update. `{ error }` shows in the dialog. |
| `onDelete` | `(id) => Promise<...>` | required | Runs after the confirm. |
| `onToggle` | `(id, enabled) => Promise<...>` | | Shows the enabled switch in the table. |
| `labels` | `Partial<ProxyHostsLabels>` | | Override any string. |

**Helpers** (pure, tested): `isValidUpstream` (needs `http://` or `https://`), `parseHosts`, `validateProxyHost`, `tlsModeAllowsWebsockets`.

## Examples

**Validate before sending**

```tsx
import { parseHosts, validateProxyHost } from "@fadymondy/nasaq/web";

const errors = validateProxyHost({ hosts: parseHosts("a.example.com, b.example.com"), upstream: "http://10.0.0.5:3000" });
// [] when valid, otherwise ["hosts"] and/or ["upstream"]
```

## Accessibility

- The enabled switch has a name that includes the host. Status is an icon and a word.
- The dialog traps focus and returns it; invalid fields are described by their message.
- The row menu is also a button in the row.

## RTL & i18n

- English and Arabic strings follow the Nasaq locale. Hostnames and upstream URLs stay left to right.

## Styling & tokens

- Built on `DataTable`, `Dialog`, `Switch`, `Select`, `Badge` and `--nq-*` tokens. Target `[data-slot="proxy-hosts"]`.

## Do / Don't

- Do validate the upstream on the server too.
- Do show offline hosts clearly: they are the ones people look for.
- Don't enable websockets for passthrough hosts: it is disabled for you.
- Don't delete without the confirm.

## Related

- [`domains-manager`](https://docs.nasaqui.com/components/domains-manager)
- [`network-rules`](https://docs.nasaqui.com/components/network-rules)
- [`cert-monitor`](https://docs.nasaqui.com/components/cert-monitor)

## Lab

https://docs.nasaqui.com/?path=/docs/components-server-tools-proxy-hosts--docs

## Code

### React

```tsx
import { ProxyHosts, type ProxyHost } from "@fadymondy/nasaq/web";

declare const hosts: ProxyHost[];
declare const api: { save(input: unknown, id?: string): Promise<void>; remove(id: string): Promise<void> };

export const Proxy = () => (
  <ProxyHosts hosts={hosts} onSave={(input, id) => api.save(input, id)} onDelete={(id) => api.remove(id)} />
);
```

### shadcn

```tsx
import { ProxyHosts, type ProxyHost } from "@/components/ui/proxy-hosts";

declare const hosts: ProxyHost[];
declare const api: { save(input: unknown, id?: string): Promise<void>; remove(id: string): Promise<void> };

export const Proxy = () => (
  <ProxyHosts hosts={hosts} onSave={(input, id) => api.save(input, id)} onDelete={(id) => api.remove(id)} />
);
```

### Vue

```vue
<script setup lang="ts">
import { NqProxyHosts, type ProxyHost, type ProxyHostInput } from "@fadymondy/nasaq/vue";
import { ref } from "vue";

const hosts = ref<ProxyHost[]>([
  { id: "h1", hosts: ["app.example.com", "www.example.com"], upstream: "http://10.0.0.5:3000", tlsMode: "auto", websockets: true, enabled: true, status: "online" },
  { id: "h2", hosts: ["api.example.com"], upstream: "https://api.internal", tlsMode: "custom", websockets: false, enabled: true, status: "unknown" },
  { id: "h3", hosts: ["old.example.org"], upstream: "http://localhost:8080", tlsMode: "off", websockets: false, enabled: false, status: "offline" },
]);

async function onSave(input: ProxyHostInput, id?: string) {
  hosts.value = id ? hosts.value.map((h) => (h.id === id ? { ...h, ...input } : h)) : [...hosts.value, { ...input, id: `h${hosts.value.length + 1}`, status: "unknown" }];
}
async function onDelete(id: string) {
  hosts.value = hosts.value.filter((h) => h.id !== id);
}
async function onToggle(id: string, enabled: boolean) {
  hosts.value = hosts.value.map((h) => (h.id === id ? { ...h, enabled } : h));
}
</script>

<template>
  <NqProxyHosts :hosts="hosts" :on-save="onSave" :on-delete="onDelete" :on-toggle="onToggle" />
</template>
```

### Blade

```blade
@php
    $hosts = [
        ['id' => 'h1', 'hosts' => ['app.example.com', 'www.example.com'], 'upstream' => 'http://10.0.0.5:3000', 'tlsMode' => 'auto', 'websockets' => true, 'enabled' => true, 'status' => 'online'],
        ['id' => 'h2', 'hosts' => ['api.example.com'], 'upstream' => 'https://api.internal', 'tlsMode' => 'custom', 'websockets' => false, 'enabled' => true, 'status' => 'unknown'],
        ['id' => 'h3', 'hosts' => ['old.example.org'], 'upstream' => 'http://localhost:8080', 'tlsMode' => 'off', 'websockets' => false, 'enabled' => false, 'status' => 'offline'],
        ['id' => 'h4', 'hosts' => ['shop.example.com', 'store.example.com', 'cdn.example.com', 'img.example.com', 'static.example.com'], 'upstream' => 'http://10.0.0.9:4000', 'tlsMode' => 'auto', 'websockets' => false, 'enabled' => true, 'status' => 'online'],
    ];
@endphp
<x-nq::proxy-hosts :hosts="$hosts"
    x-on:save-host="$event.detail.wait(Promise.resolve({}))"
    x-on:delete-host="$event.detail.wait(Promise.resolve())"
    x-on:toggle-host="$event.detail.wait(Promise.resolve())" />
```

### HTML + Alpine

```html
<div data-slot="proxy-hosts" x-data="nqProxyHosts(JSON.parse('{\u0022hosts\u0022:[{\u0022id\u0022:\u0022h1\u0022,\u0022hosts\u0022:[\u0022app.example.com\u0022,\u0022www.example.com\u0022],\u0022upstream\u0022:\u0022http:\\\/\\\/10.0.0.5:3000\u0022,\u0022tlsMode\u0022:\u0022auto\u0022,\u0022websockets\u0022:true,\u0022enabled\u0022:true,\u0022status\u0022:\u0022online\u0022},{\u0022id\u0022:\u0022h2\u0022,\u0022hosts\u0022:[\u0022api.example.com\u0022],\u0022upstream\u0022:\u0022https:\\\/\\\/api.internal\u0022,\u0022tlsMode\u0022:\u0022custom\u0022,\u0022websockets\u0022:false,\u0022enabled\u0022:true,\u0022status\u0022:\u0022unknown\u0022},{\u0022id\u0022:\u0022h3\u0022,\u0022hosts\u0022:[\u0022old.example.org\u0022],\u0022upstream\u0022:\u0022http:\\\/\\\/localhost:8080\u0022,\u0022tlsMode\u0022:\u0022off\u0022,\u0022websockets\u0022:false,\u0022enabled\u0022:false,\u0022status\u0022:\u0022offline\u0022},{\u0022id\u0022:\u0022h4\u0022,\u0022hosts\u0022:[\u0022shop.example.com\u0022,\u0022store.example.com\u0022,\u0022cdn.example.com\u0022,\u0022img.example.com\u0022,\u0022static.example.com\u0022],\u0022upstream\u0022:\u0022http:\\\/\\\/10.0.0.9:4000\u0022,\u0022tlsMode\u0022:\u0022auto\u0022,\u0022websockets\u0022:false,\u0022enabled\u0022:true,\u0022status\u0022:\u0022online\u0022}],\u0022can\u0022:{\u0022toggle\u0022:true},\u0022labels\u0022:{\u0022genericError\u0022:\u0022Something went wrong. Try again.\u0022,\u0022deleteTitle\u0022:\u0022Delete the proxy host for {name}?\u0022,\u0022more\u0022:\u0022+{n} more\u0022,\u0022moreLabel\u0022:\u0022Show {n} more domains\u0022}}'))" x-on:nq-data-table-action="onAction($event)" x-on:nq-data-table-edit="onEdit($event)"
    x-on:save-host="$event.detail.wait(Promise.resolve({}))" x-on:delete-host="$event.detail.wait(Promise.resolve())" x-on:toggle-host="$event.detail.wait(Promise.resolve())" class="grid w-full gap-3">
    <template x-if="notice"><div data-slot="alert" data-tone="danger" role="alert"
     x-data="nqAlert()" x-modelable="open" x-show="open"     x-on:nq:dismiss="notice = null" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground"><span x-text="notice"></span></div>
            </div>
            <div data-slot="alert-actions" class="ms-3 flex items-center gap-1">
                            <button data-slot="button"
     type="button"                         aria-label="Dismiss" x-on:click="dismiss()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 hover:bg-nq-hover size-control-sm p-0 text-muted-foreground [&_svg]:size-3.5">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg></button>
                    </div>
    </div>
</template>
    <div data-slot="data-table" x-data="nqDataTable([], JSON.parse('[{\u0022id\u0022:\u0022hosts\u0022,\u0022key\u0022:\u0022hostsText\u0022,\u0022header\u0022:\u0022Domains\u0022,\u0022sortable\u0022:true,\u0022searchable\u0022:true,\u0022hideable\u0022:false,\u0022type\u0022:\u0022text\u0022},{\u0022id\u0022:\u0022upstream\u0022,\u0022header\u0022:\u0022Forwards to\u0022,\u0022type\u0022:\u0022mono\u0022,\u0022sortable\u0022:true,\u0022searchable\u0022:true},{\u0022id\u0022:\u0022tls\u0022,\u0022header\u0022:\u0022TLS\u0022,\u0022type\u0022:\u0022tag\u0022,\u0022sortable\u0022:true,\u0022options\u0022:[{\u0022value\u0022:\u0022off\u0022,\u0022label\u0022:\u0022Off\u0022},{\u0022value\u0022:\u0022auto\u0022,\u0022label\u0022:\u0022Auto\u0022},{\u0022value\u0022:\u0022custom\u0022,\u0022label\u0022:\u0022Custom\u0022},{\u0022value\u0022:\u0022passthrough\u0022,\u0022label\u0022:\u0022Passthrough\u0022}]},{\u0022id\u0022:\u0022ws\u0022,\u0022header\u0022:\u0022WebSockets\u0022,\u0022type\u0022:\u0022boolean\u0022},{\u0022id\u0022:\u0022status\u0022,\u0022header\u0022:\u0022Status\u0022,\u0022type\u0022:\u0022status\u0022,\u0022sortable\u0022:true,\u0022options\u0022:[{\u0022value\u0022:\u0022online\u0022,\u0022label\u0022:\u0022Online\u0022,\u0022tone\u0022:\u0022success\u0022},{\u0022value\u0022:\u0022offline\u0022,\u0022label\u0022:\u0022Unreachable\u0022,\u0022tone\u0022:\u0022danger\u0022},{\u0022value\u0022:\u0022unknown\u0022,\u0022label\u0022:\u0022Not checked\u0022,\u0022tone\u0022:\u0022neutral\u0022}]},{\u0022id\u0022:\u0022enabled\u0022,\u0022header\u0022:\u0022Enabled\u0022,\u0022type\u0022:\u0022boolean\u0022,\u0022align\u0022:\u0022end\u0022,\u0022edit\u0022:\u0022switch\u0022}]'), JSON.parse('{\u0022nameKey\u0022:\u0022hostsText\u0022,\u0022locale\u0022:\u0022en\u0022,\u0022actions\u0022:[{\u0022id\u0022:\u0022edit\u0022,\u0022group\u0022:\u0022edit\u0022},{\u0022id\u0022:\u0022delete\u0022,\u0022group\u0022:\u0022danger\u0022}],\u0022labels\u0022:{\u0022selectRow\u0022:\u0022Select {name}\u0022,\u0022rowActions\u0022:\u0022Actions for {name}\u0022,\u0022selected\u0022:\u0022{n} selected\u0022,\u0022range\u0022:\u0022{from}\\u2013{to} of {total}\u0022,\u0022editCell\u0022:\u0022{column}, {row}\u0022,\u0022invalidNumber\u0022:\u0022Enter a number\u0022,\u0022invalidDate\u0022:\u0022Enter a date\u0022,\u0022saving\u0022:\u0022Saving\\u2026\u0022,\u0022saved\u0022:\u0022Saved\u0022,\u0022saveFailed\u0022:\u0022Couldn\\u0027t save the change\u0022,\u0022saveFailedFor\u0022:\u0022Couldn\\u0027t save: {message}\u0022,\u0022expandRow\u0022:\u0022Show details for {name}\u0022,\u0022details\u0022:\u0022Details for {name}\u0022,\u0022ascending\u0022:\u0022ascending\u0022,\u0022descending\u0022:\u0022descending\u0022,\u0022rangeAtLeast\u0022:\u0022\\u2265 {v}\u0022,\u0022rangeAtMost\u0022:\u0022\\u2264 {v}\u0022,\u0022sortPriority\u0022:\u0022sort {n}, {dir}\u0022}}'))" x-modelable="rows"
    x-model="table.rows" class="flex min-w-0 flex-col gap-3">
            <div data-slot="data-table-toolbar" class="flex flex-wrap items-center gap-2">
                            <div data-slot="data-table-search" class="relative w-full sm:w-64">
                    <svg aria-hidden="true" class="pointer-events-none absolute start-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m21 21-4.34-4.34"/>
  <circle cx="11" cy="11" r="8"/>
</svg>                    <input data-slot="input" type="search"
                    x-model="query" x-on:keydown.escape="if (query) { $event.preventDefault(); query = &#039;&#039; }" placeholder="Search hosts" aria-label="Search hosts" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control-sm ps-8 pe-8 text-body-sm [&::-webkit-search-cancel-button]:hidden">
                    <button type="button" x-show="query" aria-label="Clear search" x-on:click="query = ''" style="display: none"
                        class="absolute end-1.5 top-1/2 inline-flex size-5 -translate-y-1/2 items-center justify-center rounded-full text-muted-foreground outline-none hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus">
                        <svg aria-hidden="true" class="size-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg>                    </button>
                </div>
            <button data-slot="button"
     type="button"                         x-on:click="openForm(null)" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control-sm px-2.5 ms-auto">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
  <path d="M12 5v14"/>
</svg>Add proxy host</button>
                    </div>
    <div data-slot="table-container" role="region" tabindex="0" aria-label="Proxy hosts"                 class="relative w-full overflow-x-auto outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus">
        <div data-slot="table" role="table" aria-label="Proxy hosts"  x-bind:style="'grid-template-columns: repeat(' + (shownCount() + 1) + ', auto)'"  x-bind:data-density="density"               class="grid w-full min-w-max text-body-sm">
            <div data-slot="table-header" role="rowgroup" class="contents">
                <div role="row" data-slot="table-row" class="col-span-full grid grid-cols-subgrid border-b border-border">
                                                                                                            <div role="columnheader" data-slot="table-head" data-col="hosts" x-show="shown['hosts']"  x-bind:class="pad"  x-bind:aria-sort="ariaSort('hosts')"                             class="flex items-center h-row text-start align-middle text-caption font-medium whitespace-nowrap text-muted-foreground">
                                                            <button type="button"  x-on:click="toggleSort('hosts', $event.shiftKey)"
                                    x-bind:class="sortOf('hosts') ? 'text-foreground' : ''"
                                    class="-mx-1.5 inline-flex h-7 max-w-full items-center gap-1 rounded-control px-1.5 outline-none transition-colors duration-150 ease-nq hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus ">
                                    <span >Domains</span>
                                    <svg aria-hidden="true" x-show="! sortOf('hosts')" class="size-3.5 shrink-0 opacity-40" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg>                                    <x-lucide-arrow-up aria-hidden="true" class="size-3.5 shrink-0" x-show="sortOf('hosts') === 'asc'" style="display: none" />
                                    <x-lucide-arrow-down aria-hidden="true" class="size-3.5 shrink-0" x-show="sortOf('hosts') === 'desc'" style="display: none" />
                                                                    </button>
                                                                                </div>
                                                                    <div role="columnheader" data-slot="table-head" data-col="upstream" x-show="shown['upstream']"  x-bind:class="pad"  x-bind:aria-sort="ariaSort('upstream')"                             class="flex items-center h-row text-start align-middle text-caption font-medium whitespace-nowrap text-muted-foreground">
                                                            <button type="button"  x-on:click="toggleSort('upstream', $event.shiftKey)"
                                    x-bind:class="sortOf('upstream') ? 'text-foreground' : ''"
                                    class="-mx-1.5 inline-flex h-7 max-w-full items-center gap-1 rounded-control px-1.5 outline-none transition-colors duration-150 ease-nq hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus ">
                                    <span >Forwards to</span>
                                    <svg aria-hidden="true" x-show="! sortOf('upstream')" class="size-3.5 shrink-0 opacity-40" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg>                                    <x-lucide-arrow-up aria-hidden="true" class="size-3.5 shrink-0" x-show="sortOf('upstream') === 'asc'" style="display: none" />
                                    <x-lucide-arrow-down aria-hidden="true" class="size-3.5 shrink-0" x-show="sortOf('upstream') === 'desc'" style="display: none" />
                                                                    </button>
                                                                                </div>
                                                                    <div role="columnheader" data-slot="table-head" data-col="tls" x-show="shown['tls']"  x-bind:class="pad"  x-bind:aria-sort="ariaSort('tls')"                             class="flex items-center h-row text-start align-middle text-caption font-medium whitespace-nowrap text-muted-foreground">
                                                            <button type="button"  x-on:click="toggleSort('tls', $event.shiftKey)"
                                    x-bind:class="sortOf('tls') ? 'text-foreground' : ''"
                                    class="-mx-1.5 inline-flex h-7 max-w-full items-center gap-1 rounded-control px-1.5 outline-none transition-colors duration-150 ease-nq hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus ">
                                    <span >TLS</span>
                                    <svg aria-hidden="true" x-show="! sortOf('tls')" class="size-3.5 shrink-0 opacity-40" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg>                                    <x-lucide-arrow-up aria-hidden="true" class="size-3.5 shrink-0" x-show="sortOf('tls') === 'asc'" style="display: none" />
                                    <x-lucide-arrow-down aria-hidden="true" class="size-3.5 shrink-0" x-show="sortOf('tls') === 'desc'" style="display: none" />
                                                                    </button>
                                                                                </div>
                                                                    <div role="columnheader" data-slot="table-head" data-col="ws" x-show="shown['ws']"  x-bind:class="pad"                             class="flex items-center h-row text-start align-middle text-caption font-medium whitespace-nowrap text-muted-foreground">
                                                            WebSockets
                                                                                </div>
                                                                    <div role="columnheader" data-slot="table-head" data-col="status" x-show="shown['status']"  x-bind:class="pad"  x-bind:aria-sort="ariaSort('status')"                             class="flex items-center h-row text-start align-middle text-caption font-medium whitespace-nowrap text-muted-foreground">
                                                            <button type="button"  x-on:click="toggleSort('status', $event.shiftKey)"
                                    x-bind:class="sortOf('status') ? 'text-foreground' : ''"
                                    class="-mx-1.5 inline-flex h-7 max-w-full items-center gap-1 rounded-control px-1.5 outline-none transition-colors duration-150 ease-nq hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus ">
                                    <span >Status</span>
                                    <svg aria-hidden="true" x-show="! sortOf('status')" class="size-3.5 shrink-0 opacity-40" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg>                                    <x-lucide-arrow-up aria-hidden="true" class="size-3.5 shrink-0" x-show="sortOf('status') === 'asc'" style="display: none" />
                                    <x-lucide-arrow-down aria-hidden="true" class="size-3.5 shrink-0" x-show="sortOf('status') === 'desc'" style="display: none" />
                                                                    </button>
                                                                                </div>
                                                                    <div role="columnheader" data-slot="table-head" data-col="enabled" x-show="shown['enabled']"  x-bind:class="pad"                             class="flex items-center h-row text-end justify-end align-middle text-caption font-medium whitespace-nowrap text-muted-foreground">
                                                            Enabled
                                                                                </div>
                                                                <div role="columnheader" data-slot="table-head"  x-bind:class="pad" class="flex items-center h-row w-12 text-start align-middle text-caption font-medium whitespace-nowrap text-muted-foreground"><span class="sr-only">Actions</span></div>
                                    </div>
            </div>
                            <div role="rowgroup" class="contents" data-slot="table-body" x-show="rowCount === 0" style="display: none">
                    <div role="row" data-slot="table-row" class="col-span-full grid grid-cols-subgrid border-0">
                        <div role="cell" data-slot="table-cell" class="col-span-full h-auto p-0 whitespace-normal">
                            <div x-show="isFiltered" style="display: none">
                                <div data-slot="empty-state" class="flex flex-col items-center justify-center gap-3 border-dashed border-border px-6 py-12 text-center rounded-card border-0">
            <span data-slot="state-icon" class="inline-flex size-10 items-center justify-center rounded-control border border-border bg-card [&amp;_svg]:size-5 text-muted-foreground">
            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m21 21-4.34-4.34"/>
  <circle cx="11" cy="11" r="8"/>
</svg>        </span>
        <div class="flex max-w-sm flex-col gap-1">
        <p class="text-label text-foreground">No matching results</p>
                    <p class="text-body-sm text-muted-foreground">Try a different search or clear the filters.</p>
            </div>
    <button type="button" x-on:click="resetFilters()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-border bg-card text-foreground font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none hover:bg-nq-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 h-control-sm px-2.5">Clear filters</button>
    </div>
                            </div>
                            <div x-show="! isFiltered" style="display: none">
                                <div data-slot="empty-state" class="flex flex-col items-center justify-center gap-3 border-dashed border-border px-6 py-12 text-center rounded-card border-0">
            <span data-slot="state-icon" class="inline-flex size-10 items-center justify-center rounded-control border border-border bg-card [&amp;_svg]:size-5 text-muted-foreground">
            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <rect x="16" y="16" width="6" height="6" rx="1"/>
  <rect x="2" y="16" width="6" height="6" rx="1"/>
  <rect x="9" y="2" width="6" height="6" rx="1"/>
  <path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/>
  <path d="M12 12V8"/>
</svg>        </span>
        <div class="flex max-w-sm flex-col gap-1">
        <p class="text-label text-foreground">No proxy hosts yet. Add one to forward a domain to an app.</p>
            </div>
    </div>                            </div>
                        </div>
                    </div>
                </div>
                <template x-for="(row, index) in pageRows" x-bind:key="rid(row)">
                    <div role="rowgroup" class="contents" data-slot="table-body">
                        <div role="row" data-slot="table-row" data-row tabindex="-1" x-bind:tabindex="index === Math.min(active, pageRows.length - 1) ? 0 : -1"
                            x-bind:data-state="isSelected(row) ? 'selected' : null" x-bind:aria-selected="isSelected(row) ? 'true' : null"
                            x-bind:aria-expanded="canExpand(row) ? String(isOpen(row)) : null" x-on:focus="if ($event.target === $event.currentTarget) active = index"
                            x-on:keydown="rowKey($event, row, index, false)"
                             x-on:contextmenu="rowContext(row, $event)" x-on:touchstart.passive="rowTouch(row, $event)" x-on:touchmove.passive="rowTouchEnd()" x-on:touchend="rowTouchEnd()" x-on:touchcancel="rowTouchEnd()"                                                         x-bind:class="(isOpen(row) ? 'border-b-0 ' : '') + (ctxOpen && ctxRow === row ? 'bg-nq-hover' : '')"
                            class="col-span-full grid grid-cols-subgrid group/row border-b border-border outline-none transition-colors duration-150 ease-nq focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus data-[state=selected]:bg-nq-selected hover:bg-nq-hover ">
                                                                                            <div role="cell" data-slot="table-cell" data-cell-col="hosts" x-show="shown['hosts']"  x-bind:class="pad"
                                                                        class="flex items-center relative h-row align-middle whitespace-nowrap  ">
                                                                                                                    <span  class="flex min-w-0 items-center gap-1.5 " >
                                                                                            <span class="flex flex-wrap items-center gap-1.5">
                <template x-for="h in row.shown" x-bind:key="h"><span class="inline-flex items-center rounded-control border border-border bg-secondary px-2 py-0.5 text-body-sm text-foreground"><bdi dir="ltr" x-text="h"></bdi></span></template>
                <span x-show="row.more" style="display: none" data-slot="domain-chips-more">
                    <div data-slot="popover" x-data="nqPopover(false)" x-modelable="open" x-id="['nq-popover']" class="contents">
    <button data-slot="popover-trigger"
     type="button"                         x-bind:data-popup-open="open ? &#039;&#039; : undefined" x-ref="trigger" aria-haspopup="dialog" x-on:click="toggle()" :aria-expanded="open" x-bind:aria-label="row.moreLabel" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap border font-sans transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-6 rounded-[4px] px-1.5 text-caption">
        <bdi x-text="row.more"></bdi></button>
                        <template x-teleport="body">
    <div data-slot="popover-content" x-bind="popup" x-init="popupEl = $el" x-nq-presence="open" x-anchor.bottom-start.offset.6="$refs.trigger"
        class="z-50 max-w-[var(--available-width)] rounded-floating border border-border bg-popover p-4 text-body-sm text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0 w-auto min-w-52"><ul class="grid gap-1.5">
                                <template x-for="h in row.rest" x-bind:key="h"><li data-slot="domain-chip"><span class="inline-flex items-center rounded-control border border-border bg-secondary px-2 py-0.5 text-body-sm text-foreground"><bdi dir="ltr" class="font-mono" x-text="h"></bdi></span></li></template>
                            </ul></div>
</template>
</div>
                </span>
            </span>
                                                                                                                                </span>
                                                                    </div>
                                                                                            <div role="cell" data-slot="table-cell" data-cell-col="upstream" x-show="shown['upstream']"  x-bind:class="pad"
                                                                        class="flex items-center relative h-row align-middle whitespace-nowrap  ">
                                                                                                                    <span  class="flex min-w-0 items-center gap-1.5 " >
                                                                                            <bdi dir="ltr" class="min-w-0 truncate font-mono text-code" x-text="shownText(row, col(&#039;upstream&#039;))"></bdi>
                                                                                                                                </span>
                                                                    </div>
                                                                                            <div role="cell" data-slot="table-cell" data-cell-col="tls" x-show="shown['tls']"  x-bind:class="pad"
                                                                        class="flex items-center relative h-row align-middle whitespace-nowrap  ">
                                                                                                                    <span  class="flex min-w-0 items-center gap-1.5 " >
                                                                                            <span data-slot="badge" x-show="shownText(row, col(&#039;tls&#039;)) !== ''" x-bind:style="hueStyle(row, col(&#039;tls&#039;))" style="display: none"
                                                    class="inline-flex h-5 shrink-0 items-center gap-1 whitespace-nowrap rounded-[4px] border border-transparent bg-[var(--tag-soft)] px-1.5 text-caption font-medium text-[var(--tag-solid)]" x-text="shownText(row, col(&#039;tls&#039;))"></span>
                                                                                                                                </span>
                                                                    </div>
                                                                                            <div role="cell" data-slot="table-cell" data-cell-col="ws" x-show="shown['ws']"  x-bind:class="pad"
                                                                        class="flex items-center relative h-row align-middle whitespace-nowrap  ">
                                                                                                                    <span  class="flex min-w-0 items-center gap-1.5 " >
                                                                                            <span x-text="shownValue(row, col(&#039;ws&#039;)) === true ? &#039;Yes&#039; : (shownValue(row, col(&#039;ws&#039;)) === false ? &#039;No&#039; : '')"></span>
                                                                                                                                </span>
                                                                    </div>
                                                                                            <div role="cell" data-slot="table-cell" data-cell-col="status" x-show="shown['status']"  x-bind:class="pad"
                                                                        class="flex items-center relative h-row align-middle whitespace-nowrap  ">
                                                                                                                    <span  class="flex min-w-0 items-center gap-1.5 " >
                                                                                                                                                <span data-slot="status" data-tone="neutral" x-show="shownText(row, col(&#039;status&#039;)) !== '' && tone(row, col(&#039;status&#039;)) === 'neutral'" style="display: none" class="inline-flex min-w-0 items-center gap-1.5 text-body-sm text-foreground">
                                                        <svg aria-hidden="true" class="size-3.5 shrink-0 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
</svg>                                                        <span class="truncate" x-text="shownText(row, col(&#039;status&#039;))"></span>
                                                    </span>
                                                                                                    <span data-slot="status" data-tone="info" x-show="shownText(row, col(&#039;status&#039;)) !== '' && tone(row, col(&#039;status&#039;)) === 'info'" style="display: none" class="inline-flex min-w-0 items-center gap-1.5 text-body-sm text-foreground">
                                                        <svg aria-hidden="true" class="size-3.5 shrink-0 text-nq-info-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <circle cx="12" cy="12" r="1"/>
</svg>                                                        <span class="truncate" x-text="shownText(row, col(&#039;status&#039;))"></span>
                                                    </span>
                                                                                                    <span data-slot="status" data-tone="success" x-show="shownText(row, col(&#039;status&#039;)) !== '' && tone(row, col(&#039;status&#039;)) === 'success'" style="display: none" class="inline-flex min-w-0 items-center gap-1.5 text-body-sm text-foreground">
                                                        <svg aria-hidden="true" class="size-3.5 shrink-0 text-nq-success-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m9 12 2 2 4-4"/>
</svg>                                                        <span class="truncate" x-text="shownText(row, col(&#039;status&#039;))"></span>
                                                    </span>
                                                                                                    <span data-slot="status" data-tone="warning" x-show="shownText(row, col(&#039;status&#039;)) !== '' && tone(row, col(&#039;status&#039;)) === 'warning'" style="display: none" class="inline-flex min-w-0 items-center gap-1.5 text-body-sm text-foreground">
                                                        <svg aria-hidden="true" class="size-3.5 shrink-0 text-nq-warning-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <line x1="12" x2="12" y1="8" y2="12"/>
  <line x1="12" x2="12.01" y1="16" y2="16"/>
</svg>                                                        <span class="truncate" x-text="shownText(row, col(&#039;status&#039;))"></span>
                                                    </span>
                                                                                                    <span data-slot="status" data-tone="danger" x-show="shownText(row, col(&#039;status&#039;)) !== '' && tone(row, col(&#039;status&#039;)) === 'danger'" style="display: none" class="inline-flex min-w-0 items-center gap-1.5 text-body-sm text-foreground">
                                                        <svg aria-hidden="true" class="size-3.5 shrink-0 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>                                                        <span class="truncate" x-text="shownText(row, col(&#039;status&#039;))"></span>
                                                    </span>
                                                                                                                                                                                </span>
                                                                    </div>
                                                                                            <div role="cell" data-slot="table-cell" data-cell-col="enabled" x-show="shown['enabled']"  x-bind:class="pad"
                                                                            x-bind:data-cell-row="rid(row)" tabindex="-1" x-bind:data-editable="editable(row, col(&#039;enabled&#039;)) ? '' : null" x-bind:data-editing="isEditing(row, col(&#039;enabled&#039;)) ? '' : null"
                                        x-bind:aria-busy="isPending(row, col(&#039;enabled&#039;)) ? 'true' : null" x-bind:aria-invalid="failure(row, col(&#039;enabled&#039;)) ? 'true' : null"
                                        x-on:dblclick="beginEdit(row, col(&#039;enabled&#039;), $event)" x-on:keydown.enter.self.prevent="beginEdit(row, col(&#039;enabled&#039;), $event)" x-on:keydown.f2.self.prevent="beginEdit(row, col(&#039;enabled&#039;), $event)"
                                        x-bind:class="failure(row, col(&#039;enabled&#039;)) ? 'bg-nq-danger-soft' : ''"
                                                                        class="flex items-center relative h-row align-middle whitespace-nowrap text-end justify-end outline-none focus:outline-2 focus:-outline-offset-2 focus:outline-nq-focus">
                                                                            <button type="button" role="switch" data-slot="switch" x-bind:aria-label="editName(row, col(&#039;enabled&#039;))" x-on:click="toggleSwitch(row, col(&#039;enabled&#039;))"
                                            x-bind:aria-checked="shownValue(row, col(&#039;enabled&#039;)) === true ? 'true' : 'false'" aria-checked="false"
                                            x-bind:data-checked="shownValue(row, col(&#039;enabled&#039;)) === true ? '' : null" x-bind:data-unchecked="shownValue(row, col(&#039;enabled&#039;)) === true ? null : ''"
                                            x-bind:disabled="editable(row, col(&#039;enabled&#039;)) ? null : ''" x-bind:data-disabled="editable(row, col(&#039;enabled&#039;)) ? null : ''"
                                            class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent bg-nq-line-strong p-0.5 outline-none transition-colors duration-150 ease-nq data-checked:bg-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:cursor-not-allowed data-disabled:opacity-50">
                                            <span data-slot="switch-thumb" x-bind:data-checked="shownValue(row, col(&#039;enabled&#039;)) === true ? '' : null" x-bind:data-unchecked="shownValue(row, col(&#039;enabled&#039;)) === true ? null : ''"
                                                class="block size-4 rounded-full bg-background shadow-xs data-checked:bg-primary-foreground transition-[translate] duration-150 ease-nq data-checked:translate-x-3.5 rtl:data-checked:-translate-x-3.5"></span>
                                        </button>
                                                                    </div>
                                                            <div role="cell" data-slot="table-cell"  x-bind:class="pad" class="flex items-center h-row w-12 pe-2 text-end align-middle whitespace-nowrap">
                                    <div data-slot="dropdown-menu" x-data="nqDropdownMenu(false)" x-modelable="open" class="contents"><button data-slot="data-table-row-actions"
     type="button"                         x-bind="trigger" x-ref="trigger" x-bind:aria-label="say(&#039;rowActions&#039;, row)" x-show="hasActions(row)" x-bind:tabindex="index === Math.min(active, pageRows.length - 1) ? 0 : -1" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-nq-hover size-control-sm p-0 text-muted-foreground opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100 group-data-[state=selected]/row:opacity-100 data-popup-open:opacity-100 pointer-coarse:opacity-100">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="1"/>
  <circle cx="19" cy="12" r="1"/>
  <circle cx="5" cy="12" r="1"/>
</svg></button>
                                        <template x-teleport="body">
    <div data-slot="dropdown-menu-content" x-bind="popup" x-init="popupEl = $el" x-nq-presence="open" x-anchor.bottom-end.offset.4="$refs.trigger"
        class="z-50 overflow-hidden rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none max-h-[var(--available-height)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0 min-w-44"><div data-slot="dropdown-menu-group" role="group" ><template x-if="actionOn(row, 0)">
                                                        <div role="none" class="contents">
                                                                                                                        <div data-slot="dropdown-menu-item" role="menuitem" data-variant="default" x-bind="item"
        x-on:click="act(&#039;edit&#039;, row)" x-bind:data-disabled="actionOff(row, 0) ? &#039;&#039; : null" x-bind:aria-disabled="actionOff(row, 0) ? &#039;true&#039; : null" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/>
  <path d="m15 5 4 4"/>
</svg>                                                                Edit</div>
                                                        </div>
                                                    </template>
                                                                                                    <template x-if="actionOn(row, 1)">
                                                        <div role="none" class="contents">
                                                            <template x-if="sepBefore(row, 1)"><div data-slot="dropdown-menu-separator" role="separator" aria-orientation="horizontal" class="-mx-1.5 my-1.5 h-px bg-border"></div>
</template>                                                            <div data-slot="dropdown-menu-item" role="menuitem" data-variant="danger" x-bind="item"
        x-on:click="act(&#039;delete&#039;, row)" x-bind:data-disabled="actionOff(row, 1) ? &#039;&#039; : null" x-bind:aria-disabled="actionOff(row, 1) ? &#039;true&#039; : null" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 text-nq-danger-text [&_svg]:text-current"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10 11v6"/>
  <path d="M14 11v6"/>
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
  <path d="M3 6h18"/>
  <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>                                                                Delete</div>
                                                        </div>
                                                    </template></div></div>
</template></div>
                                </div>
                                                    </div>
                                            </div>
                </template>
                    </div>
    </div>
                <div data-slot="data-table-context-menu" x-data="nqContextMenu()" x-effect="menuClosed(open)" class="contents">
            <template x-teleport="body">
    <div data-slot="context-menu-content" x-bind="popup" x-init="popupEl = $el" x-nq-presence="open"
        class="fixed z-50 overflow-hidden rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none max-h-[var(--available-height)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0 min-w-44"><template x-if="ctxRow && actionOn(ctxRow, 0)">
                        <div role="none" class="contents">
                                                        <div data-slot="context-menu-item" role="menuitem" data-variant="default" x-bind="item"
        x-on:click="act(&#039;edit&#039;, ctxRow)" x-bind:data-disabled="actionOff(ctxRow, 0) ? &#039;&#039; : null" x-bind:aria-disabled="actionOff(ctxRow, 0) ? &#039;true&#039; : null" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/>
  <path d="m15 5 4 4"/>
</svg>                                Edit</div>
                        </div>
                    </template>
                                    <template x-if="ctxRow && actionOn(ctxRow, 1)">
                        <div role="none" class="contents">
                            <template x-if="sepBefore(ctxRow, 1)"><div data-slot="context-menu-separator" role="separator" aria-orientation="horizontal" class="-mx-1.5 my-1.5 h-px bg-border"></div>
</template>                            <div data-slot="context-menu-item" role="menuitem" data-variant="danger" x-bind="item"
        x-on:click="act(&#039;delete&#039;, ctxRow)" x-bind:data-disabled="actionOff(ctxRow, 1) ? &#039;&#039; : null" x-bind:aria-disabled="actionOff(ctxRow, 1) ? &#039;true&#039; : null" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 text-nq-danger-text [&_svg]:text-current"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10 11v6"/>
  <path d="M14 11v6"/>
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
  <path d="M3 6h18"/>
  <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>                                Delete</div>
                        </div>
                    </template></div>
</template>
        </div>
        <span role="status" aria-live="polite" class="sr-only" x-text="announce"></span>
</div>
    <div data-slot="dialog" x-data="nqDialog(false)" x-modelable="open" x-id="['nq-dialog']" x-model="form.open" class="contents">
    <template x-teleport="body">
    <div data-slot="dialog-portal">
        <div data-slot="dialog-backdrop" x-nq-presence="open" x-on:click="close()" class="fixed inset-0 z-50 bg-nq-fg/15 dark:bg-nq-bg/60 transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0"></div>
        <div data-slot="dialog-content" x-bind="popup" x-nq-presence="open" x-trap.noscroll="open"
            class="fixed inset-0 z-50 m-auto grid h-fit w-[calc(100%-2rem)] max-w-lg gap-4 rounded-floating border border-border bg-popover p-6 text-popover-foreground outline-none max-h-[calc(100dvh-2rem)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
            <form novalidate data-slot="proxy-host-dialog" class="grid gap-4" x-on:submit.prevent="saveForm()">
                <div data-slot="dialog-header" class="flex flex-col gap-1.5 pe-8 text-start"><h2 data-slot="dialog-title" :id="$id('nq-dialog', 'title')" class="text-h3 text-foreground"><span x-show="! form.editingId">New proxy host</span><span x-show="form.editingId" style="display: none">Edit proxy host</span></h2>
                    <p data-slot="dialog-description" :id="$id('nq-dialog', 'description')" class="text-body-sm text-muted-foreground">Requests to these domains are forwarded to the upstream.</p></div>
                <template x-if="form.error"><div data-slot="alert" data-tone="danger" role="alert"
        class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground"><span x-text="form.error"></span></div>
            </div>
    </div>
</template>
                <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="form.invalid.hosts" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Domain names</label>
                    <textarea data-slot="textarea"
                x-model="form.hostsText" x-on:input="onFormInput()" dir="ltr" rows="3" placeholder="app.example.com" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-foreground transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] min-h-20 py-2 font-mono text-body-sm"></textarea>
                    <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text">Enter valid domain names, like app.example.com.</div>
                    <p data-slot="field-description" class="text-caption text-muted-foreground">One or more, separated by spaces, commas or new lines.</p>
</div>
                <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="form.invalid.upstream" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Upstream address</label>
                    <input data-slot="input" type="text"
     dir="ltr"                 x-model="form.upstream" x-on:input="onFormInput()" placeholder="http://10.0.0.5:3000" autocomplete="off" spellcheck="false" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control text-start">
                    <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text">Enter an address like http://10.0.0.5:3000.</div>
                    <p data-slot="field-description" class="text-caption text-muted-foreground">Where requests are sent, like http://10.0.0.5:3000.</p>
</div>
                <div class="flex flex-col gap-1.5">
                    <span id="proxy-tls" class="text-label text-foreground">TLS</span>
                    <div data-slot="select" x-data="nqSelect('auto', false)" x-modelable="value" x-id="['nq-select']" x-model="form.tls" class="contents">
    <button data-slot="select-trigger" x-ref="trigger" x-bind="trigger"
        aria-labelledby="proxy-tls" class="flex h-control w-full min-w-0 items-center justify-between gap-2 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] cursor-default select-none outline-none transition-colors duration-150 ease-nq focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-popup-open:border-nq-focus data-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px]">
    <span data-slot="select-value" x-text="label() ?? ''" :data-placeholder="empty() ? '' : undefined"
    class="min-w-0 flex-1 truncate text-start data-placeholder:text-muted-foreground"></span>
    <span class="flex shrink-0 text-muted-foreground [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg></span>
</button>
                        <template x-teleport="body">
    <div data-slot="select-content" x-ref="popup" x-bind="popup" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.trigger"
        class="z-50 min-w-[var(--anchor-width)] max-h-[var(--available-height)] overflow-y-auto rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
        <div data-slot="select-item" x-bind="item('off', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('off')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate">Off (HTTP only)</span>
</div>
                                                            <div data-slot="select-item" x-bind="item('auto', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('auto')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate">Automatic certificate</span>
</div>
                                                            <div data-slot="select-item" x-bind="item('custom', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('custom')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate">Custom certificate</span>
</div>
                                                            <div data-slot="select-item" x-bind="item('passthrough', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('passthrough')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate">Passthrough</span>
</div>
    </div>
</template>
    </div>
                                            <p class="text-body-sm text-muted-foreground" x-show="form.tls === 'off'"  style="display: none" >Traffic between visitors and the proxy is not encrypted.</p>
                                            <p class="text-body-sm text-muted-foreground" x-show="form.tls === 'auto'" >A certificate is issued and renewed for you.</p>
                                            <p class="text-body-sm text-muted-foreground" x-show="form.tls === 'custom'"  style="display: none" >You upload the certificate and key on the certificates page.</p>
                                            <p class="text-body-sm text-muted-foreground" x-show="form.tls === 'passthrough'"  style="display: none" >Encrypted traffic goes to the upstream untouched. The upstream must hold the certificate.</p>
                                    </div>
                <div class="flex items-start justify-between gap-4" x-bind:class="wsBlocked ? 'opacity-50' : ''">
                    <div class="min-w-0">
                        <p id="proxy-ws" class="text-label text-foreground">WebSocket support</p>
                        <p class="text-body-sm text-muted-foreground">Keeps upgraded connections open. Turn on for live apps and chat.</p>
                    </div>
                    <div x-bind:class="wsBlocked ? 'pointer-events-none' : ''" x-bind:inert="wsBlocked">
                        <button type="button" role="switch" data-slot="switch" x-data="nqSwitch(false)" x-modelable="checked" x-bind="root"
    aria-checked="false"
     data-unchecked                 aria-labelledby="proxy-ws" x-model="form.websockets" class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent bg-nq-line-strong p-0.5 outline-none transition-colors duration-150 ease-nq data-checked:bg-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:cursor-not-allowed data-disabled:opacity-50">
    <span data-slot="switch-thumb"  data-unchecked         x-bind:data-checked="checked ? '' : undefined" x-bind:data-unchecked="checked ? undefined : ''"
        class="block size-4 rounded-full bg-background shadow-xs data-checked:bg-primary-foreground transition-[translate] duration-150 ease-nq data-checked:translate-x-3.5 rtl:data-checked:-translate-x-3.5"></span>
    </button>
                    </div>
                </div>
                <div class="flex items-center justify-between gap-4">
                    <p id="proxy-enabled" class="text-label text-foreground">Enabled</p>
                    <button type="button" role="switch" data-slot="switch" x-data="nqSwitch(true)" x-modelable="checked" x-bind="root"
    aria-checked="true"
     data-checked                 aria-labelledby="proxy-enabled" x-model="form.enabled" class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent bg-nq-line-strong p-0.5 outline-none transition-colors duration-150 ease-nq data-checked:bg-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:cursor-not-allowed data-disabled:opacity-50">
    <span data-slot="switch-thumb"  data-checked         x-bind:data-checked="checked ? '' : undefined" x-bind:data-unchecked="checked ? undefined : ''"
        class="block size-4 rounded-full bg-background shadow-xs data-checked:bg-primary-foreground transition-[translate] duration-150 ease-nq data-checked:translate-x-3.5 rtl:data-checked:-translate-x-3.5"></span>
    </button>
                </div>
                <div data-slot="dialog-footer" class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"><button data-slot="button"
     type="button"                         x-on:click="form.open = false" x-bind:disabled="form.pending ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Cancel</button>
                    <button data-slot="button"
     type="submit"                         x-bind:aria-busy="form.pending ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        <svg data-slot="spinner" aria-hidden="true" style="display: none;" x-show="form.pending" class="size-4 animate-spin motion-reduce:animate-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>                        Save proxy host</button></div>
            </form>
                            <button type="button" data-slot="dialog-close" x-on:click="close()" aria-label="Close"
                    class="absolute end-3 top-3 inline-flex size-8 items-center justify-center rounded-control text-muted-foreground transition-colors duration-150 hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg></button>
                    </div>
    </div>
</template>
</div>
    <div data-slot="alert-dialog" x-data="nqAlertDialog(false)" x-modelable="open" x-id="['nq-alert-dialog']" x-model="confirm.open" class="contents">
    <template x-teleport="body">
    <div data-slot="alert-dialog-portal">
        <div data-slot="alert-dialog-backdrop" x-nq-presence="open" class="fixed inset-0 z-50 bg-nq-fg/15 dark:bg-nq-bg/60 transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0"></div>
        <div data-slot="alert-dialog-content" x-bind="popup" x-nq-presence="open" x-trap.noscroll="open"
            class="fixed inset-0 z-50 m-auto grid h-fit w-[calc(100%-2rem)] max-w-md gap-4 rounded-floating border border-border bg-popover p-6 text-popover-foreground outline-none max-h-[calc(100dvh-2rem)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
            <div data-slot="proxy-hosts-confirm" class="grid gap-4">
                <div data-slot="alert-dialog-header" class="flex flex-col gap-1.5 text-start"><h2 data-slot="alert-dialog-title" :id="$id('nq-alert-dialog', 'title')" class="text-h3 text-foreground"><span x-text="confirm.title"></span></h2>
                    <p data-slot="alert-dialog-description" :id="$id('nq-alert-dialog', 'description')" class="text-body-sm text-muted-foreground">Requests to these domains stop being forwarded. The app itself is not touched.</p></div>
                <div data-slot="alert-dialog-footer" class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"><button data-slot="alert-dialog-cancel"
     type="button"                         autofocus="autofocus" x-on:click="close()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Cancel</button>
                    <button data-slot="alert-dialog-action"
     type="button"                         x-on:click.capture="close()" x-on:click="runConfirm()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-destructive text-destructive-foreground hover:bg-[color-mix(in_oklab,var(--nq-danger-solid)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        Delete</button></div>
            </div>
        </div>
    </div>
</template>
</div>
</div>
```
