From 61419a5ada0fb1118fa1a3d354bbcb060f572b8f Mon Sep 17 00:00:00 2001 From: Masoud Mazarei Date: Sat, 7 Nov 2026 12:31:32 -0500 Subject: [PATCH 01/12] feat(ui): add phase 5 kouja storage and disk workflow Add kouja backend fields, protocol/locator/deletion states, thin-pool-aware VM gating, Terraform kouja config and remote disk fields, and operator/API documentation. --- .../internal/provider/data_source_disk.go | 4 + .../internal/provider/resource_disk.go | 43 +++++++- .../provider/resource_storage_backend.go | 87 +++++++++++++-- docs/api.md | 12 +- docs/architecture.md | 2 +- docs/operations.md | 8 +- internal/webui/static/build/app.js | 68 ++++++------ ui/src/inventory/disks/DiskDialog.tsx | 17 +-- ui/src/inventory/disks/DisksTab.tsx | 37 +++++-- .../inventory/disks/StorageBackendDialog.tsx | 104 +++++++++++++++--- ui/src/inventory/types.ts | 25 ++++- ui/src/inventory/vms/VMDialog.tsx | 12 +- ui/src/inventory/vms/VMsTab.tsx | 15 +-- ui/src/inventory/vms/disk-predicates.ts | 14 +++ 14 files changed, 354 insertions(+), 94 deletions(-) create mode 100644 ui/src/inventory/vms/disk-predicates.ts diff --git a/cmd/terraform-provider-rookery/internal/provider/data_source_disk.go b/cmd/terraform-provider-rookery/internal/provider/data_source_disk.go index 711a19e..cb7b61c 100644 --- a/cmd/terraform-provider-rookery/internal/provider/data_source_disk.go +++ b/cmd/terraform-provider-rookery/internal/provider/data_source_disk.go @@ -38,6 +38,10 @@ func (d *diskDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r "size_gb": dschema.Int64Attribute{Computed: true}, "size_bytes": dschema.Int64Attribute{Computed: true}, "source": dschema.StringAttribute{Computed: true}, + "protocol": dschema.StringAttribute{Computed: true}, + "locator": dschema.StringAttribute{Computed: true}, + "deletion_state": dschema.StringAttribute{Computed: true}, + "deletion_error": dschema.StringAttribute{Computed: true}, "host_device": dschema.StringAttribute{Computed: true}, "storage_backend_id": dschema.Int64Attribute{Computed: true}, "storage_backend_name": dschema.StringAttribute{Computed: true}, diff --git a/cmd/terraform-provider-rookery/internal/provider/resource_disk.go b/cmd/terraform-provider-rookery/internal/provider/resource_disk.go index 733e1ca..8dcf474 100644 --- a/cmd/terraform-provider-rookery/internal/provider/resource_disk.go +++ b/cmd/terraform-provider-rookery/internal/provider/resource_disk.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -41,6 +42,10 @@ type diskModel struct { ThinPool types.String `tfsdk:"thin_pool"` Device types.String `tfsdk:"device"` Source types.String `tfsdk:"source"` + Protocol types.String `tfsdk:"protocol"` + Locator types.String `tfsdk:"locator"` + DeletionState types.String `tfsdk:"deletion_state"` + DeletionError types.String `tfsdk:"deletion_error"` HostDevice types.String `tfsdk:"host_device"` AttachedVMID types.String `tfsdk:"attached_vm_id"` AttachedVMName types.String `tfsdk:"attached_vm_name"` @@ -59,7 +64,7 @@ func (r *diskResource) Metadata(_ context.Context, req resource.MetadataRequest, func (r *diskResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Description: "A thin-LVM disk or template clone in Rookery.", + Description: "A provisioned, remote, or passthrough disk in Rookery.", Attributes: map[string]schema.Attribute{ "labels": labelsResourceAttribute(), "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, @@ -85,8 +90,12 @@ func (r *diskResource) Schema(_ context.Context, _ resource.SchemaRequest, resp }, "source": schema.StringAttribute{ Computed: true, - Description: "Disk source: lvm-thin or host-device.", + Description: "Disk source (lvm-thin, host-device, kouja, ...).", }, + "protocol": schema.StringAttribute{Computed: true, Description: "Attachment protocol."}, + "locator": schema.StringAttribute{Computed: true, Description: "Display locator for remote disks."}, + "deletion_state": schema.StringAttribute{Computed: true, Description: "Asynchronous deletion state."}, + "deletion_error": schema.StringAttribute{Computed: true, Description: "Last asynchronous deletion error."}, "storage_backend_id": schema.Int64Attribute{ Optional: true, Computed: true, @@ -249,11 +258,33 @@ func (r *diskResource) Delete(ctx context.Context, req resource.DeleteRequest, r resp.Diagnostics.AddError("Delete disk", err.Error()) return } - callCtx, cancel := context.WithTimeout(ctx, client.Timeout()) + callCtx, cancel := context.WithTimeout(ctx, 4*time.Hour) defer cancel() - if _, err := client.Disks.DeleteDisk(callCtx, &rookeryv1.DeleteDiskRequest{Id: state.ID.ValueInt64()}); err != nil && !isNotFound(err) { + deleted, err := client.Disks.DeleteDisk(callCtx, &rookeryv1.DeleteDiskRequest{Id: state.ID.ValueInt64()}) + if err != nil && !isNotFound(err) { resp.Diagnostics.AddError("Delete disk", err.Error()) + return + } + if deleted == nil || !deleted.GetAccepted() { + return + } + deadline := time.Now().Add(4 * time.Hour) + for time.Now().Before(deadline) { + time.Sleep(2 * time.Second) + disk, readErr := client.Disks.GetDisk(callCtx, &rookeryv1.GetDiskRequest{Id: state.ID.ValueInt64()}) + if isNotFound(readErr) { + return + } + if readErr != nil { + resp.Diagnostics.AddError("Delete disk", readErr.Error()) + return + } + if disk.GetDeletionState() == "delete_failed" { + resp.Diagnostics.AddError("Delete disk", disk.GetDeletionError()) + return + } } + resp.Diagnostics.AddError("Delete disk", "timed out waiting for asynchronous disk deletion") } func (r *diskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -271,6 +302,10 @@ func setDiskState(state *diskModel, disk *rookeryv1.Disk) { state.ThinPool = types.StringValue(disk.GetThinPool()) state.Device = types.StringValue(disk.GetDevice()) state.Source = types.StringValue(disk.GetSource()) + state.Protocol = optionalStringValue(disk.GetProtocol()) + state.Locator = optionalStringValue(disk.GetLocator()) + state.DeletionState = optionalStringValue(disk.GetDeletionState()) + state.DeletionError = optionalStringValue(disk.GetDeletionError()) if disk.GetSource() == "host-device" { state.HostDevice = types.StringValue(disk.GetDevice()) } else { diff --git a/cmd/terraform-provider-rookery/internal/provider/resource_storage_backend.go b/cmd/terraform-provider-rookery/internal/provider/resource_storage_backend.go index c86eede..e4a52c9 100644 --- a/cmd/terraform-provider-rookery/internal/provider/resource_storage_backend.go +++ b/cmd/terraform-provider-rookery/internal/provider/resource_storage_backend.go @@ -48,6 +48,33 @@ type storageBackendConfigModel struct { PreparationScript types.String `tfsdk:"preparation_script"` PreparationTimeout types.String `tfsdk:"preparation_timeout"` AllowTemplateDeleteWithClones types.Bool `tfsdk:"allow_template_delete_with_clones"` + Kouja types.Object `tfsdk:"kouja"` +} + +type koujaConfigModel struct { + Endpoint types.String `tfsdk:"endpoint"` + Protocol types.String `tfsdk:"protocol"` + StorageClass types.String `tfsdk:"storage_class"` + Initiator types.String `tfsdk:"initiator"` + HostNQN types.String `tfsdk:"host_nqn"` + CAFile types.String `tfsdk:"ca_file"` + ClientCertFile types.String `tfsdk:"client_cert_file"` + ClientKeyFile types.String `tfsdk:"client_key_file"` + ServerName types.String `tfsdk:"server_name"` + Portal types.String `tfsdk:"portal"` +} + +var koujaConfigAttrTypes = map[string]attr.Type{ + "endpoint": types.StringType, + "protocol": types.StringType, + "storage_class": types.StringType, + "initiator": types.StringType, + "host_nqn": types.StringType, + "ca_file": types.StringType, + "client_cert_file": types.StringType, + "client_key_file": types.StringType, + "server_name": types.StringType, + "portal": types.StringType, } var storageBackendConfigAttrTypes = map[string]attr.Type{ @@ -57,6 +84,7 @@ var storageBackendConfigAttrTypes = map[string]attr.Type{ "preparation_script": types.StringType, "preparation_timeout": types.StringType, "allow_template_delete_with_clones": types.BoolType, + "kouja": types.ObjectType{AttrTypes: koujaConfigAttrTypes}, } func (r *storageBackendResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -65,7 +93,7 @@ func (r *storageBackendResource) Metadata(_ context.Context, req resource.Metada func (r *storageBackendResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Description: "A Rookery storage backend. Only the lvm-thin driver is currently supported.", + Description: "A Rookery storage backend. Supports lvm-thin and kouja drivers.", Attributes: map[string]schema.Attribute{ "labels": labelsResourceAttribute(), "id": schema.Int64Attribute{ @@ -78,15 +106,15 @@ func (r *storageBackendResource) Schema(_ context.Context, _ resource.SchemaRequ "driver": schema.StringAttribute{ Optional: true, Computed: true, - Description: "Storage driver. Defaults to lvm-thin.", + Description: "Storage driver: lvm-thin or kouja. Defaults to lvm-thin.", PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "config": schema.SingleNestedAttribute{ Required: true, - Description: "LVM thin-pool and template configuration.", + Description: "Driver-specific configuration. Use vg/thin_pool for LVM or kouja for a kouja backend.", Attributes: map[string]schema.Attribute{ - "vg": schema.StringAttribute{Required: true, Description: "Volume group."}, - "thin_pool": schema.StringAttribute{Required: true, Description: "Thin pool name."}, + "vg": schema.StringAttribute{Optional: true, Computed: true, Description: "Volume group for lvm-thin."}, + "thin_pool": schema.StringAttribute{Optional: true, Computed: true, Description: "Thin pool for lvm-thin."}, "supports_templates": schema.BoolAttribute{Optional: true, Computed: true, Description: "Allow template disks on this backend."}, "preparation_script": schema.StringAttribute{Optional: true, Computed: true, Description: "Script used to clone template disks."}, "preparation_timeout": schema.StringAttribute{Optional: true, Computed: true, Description: "Preparation script timeout."}, @@ -95,6 +123,22 @@ func (r *storageBackendResource) Schema(_ context.Context, _ resource.SchemaRequ Computed: true, Description: "Allow deleting a template while clones exist.", }, + "kouja": schema.SingleNestedAttribute{ + Optional: true, + Description: "Kouja endpoint, storage class, and mTLS client configuration.", + Attributes: map[string]schema.Attribute{ + "endpoint": schema.StringAttribute{Optional: true, Computed: true}, + "protocol": schema.StringAttribute{Optional: true, Computed: true}, + "storage_class": schema.StringAttribute{Optional: true, Computed: true}, + "initiator": schema.StringAttribute{Optional: true, Computed: true}, + "host_nqn": schema.StringAttribute{Optional: true, Computed: true}, + "ca_file": schema.StringAttribute{Optional: true, Computed: true}, + "client_cert_file": schema.StringAttribute{Optional: true, Computed: true}, + "client_key_file": schema.StringAttribute{Optional: true, Computed: true}, + "server_name": schema.StringAttribute{Optional: true, Computed: true}, + "portal": schema.StringAttribute{Optional: true, Computed: true}, + }, + }, }, }, "disk_count": schema.Int64Attribute{Computed: true, Description: "Number of disks on this backend."}, @@ -244,6 +288,19 @@ func setStorageBackendState(ctx context.Context, state *storageBackendModel, bac state.Driver = types.StringValue(backend.GetDriver()) state.DiskCount = types.Int64Value(int64(backend.GetDiskCount())) cfg := backend.GetConfig() + kouja := types.ObjectNull(koujaConfigAttrTypes) + if cfg.GetKouja() != nil { + value := koujaConfigModel{ + Endpoint: types.StringValue(cfg.GetKouja().GetEndpoint()), Protocol: optionalStringValue(cfg.GetKouja().GetProtocol()), + StorageClass: types.StringValue(cfg.GetKouja().GetStorageClass()), Initiator: optionalStringValue(cfg.GetKouja().GetInitiator()), + HostNQN: optionalStringValue(cfg.GetKouja().GetHostNqn()), CAFile: types.StringValue(cfg.GetKouja().GetCaFile()), + ClientCertFile: types.StringValue(cfg.GetKouja().GetClientCertFile()), ClientKeyFile: types.StringValue(cfg.GetKouja().GetClientKeyFile()), + ServerName: optionalStringValue(cfg.GetKouja().GetServerName()), Portal: optionalStringValue(cfg.GetKouja().GetPortal()), + } + object, objectDiags := types.ObjectValueFrom(ctx, koujaConfigAttrTypes, value) + diags.Append(objectDiags...) + kouja = object + } config := storageBackendConfigModel{ VG: types.StringValue(cfg.GetVg()), ThinPool: types.StringValue(cfg.GetThinPool()), @@ -251,6 +308,7 @@ func setStorageBackendState(ctx context.Context, state *storageBackendModel, bac PreparationScript: optionalStringValue(cfg.GetPreparationScript()), PreparationTimeout: optionalStringValue(cfg.GetPreparationTimeout()), AllowTemplateDeleteWithClones: types.BoolValue(cfg.GetAllowTemplateDeleteWithClones()), + Kouja: kouja, } object, objectDiags := types.ObjectValueFrom(ctx, storageBackendConfigAttrTypes, config) diags.Append(objectDiags...) @@ -264,12 +322,27 @@ func storageBackendConfigFromObject(ctx context.Context, object types.Object) (* if diags.HasError() { return nil, diags } - return &rookeryv1.StorageBackendConfig{ + result := &rookeryv1.StorageBackendConfig{ Vg: config.VG.ValueString(), ThinPool: config.ThinPool.ValueString(), SupportsTemplates: config.SupportsTemplates.ValueBool(), PreparationScript: config.PreparationScript.ValueString(), PreparationTimeout: config.PreparationTimeout.ValueString(), AllowTemplateDeleteWithClones: config.AllowTemplateDeleteWithClones.ValueBool(), - }, diags + } + if !config.Kouja.IsNull() && !config.Kouja.IsUnknown() { + var kouja koujaConfigModel + diags.Append(config.Kouja.As(ctx, &kouja, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + result.DriverConfig = &rookeryv1.StorageBackendConfig_Kouja{Kouja: &rookeryv1.KoujaConfig{ + Endpoint: kouja.Endpoint.ValueString(), Protocol: kouja.Protocol.ValueString(), + StorageClass: kouja.StorageClass.ValueString(), Initiator: kouja.Initiator.ValueString(), + HostNqn: kouja.HostNQN.ValueString(), CaFile: kouja.CAFile.ValueString(), + ClientCertFile: kouja.ClientCertFile.ValueString(), ClientKeyFile: kouja.ClientKeyFile.ValueString(), + ServerName: kouja.ServerName.ValueString(), Portal: kouja.Portal.ValueString(), + }} + } + return result, diags } diff --git a/docs/api.md b/docs/api.md index bb4544c..32032cd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -169,7 +169,7 @@ labels in sync through the same API. | `PUT` | `/api/cgroup-settings` | Apply host-wide cgroup v2 limits and classes | | `GET` | `/api/cgroup-usage` | Live memory, CPU, task, and I/O usage per cgroup node | | `GET` | `/api/storage-backends` | List registered storage backends | -| `POST` | `/api/storage-backends` | Register an LVM thin-pool backend | +| `POST` | `/api/storage-backends` | Register an LVM thin-pool or kouja backend | | `GET` | `/api/storage-backends/{id}` | Get one storage backend | | `PUT` | `/api/storage-backends/{id}` | Update an unused backend | | `DELETE` | `/api/storage-backends/{id}` | Delete a backend with no disks | @@ -179,7 +179,7 @@ labels in sync through the same API. | `POST` | `/api/disks` | Create a thin-LVM disk, host passthrough disk, template, or provisioned template clone | | `GET` | `/api/disks/{id}` | Get one disk | | `PUT` | `/api/disks/{id}` | Update template status or template provisioning settings | -| `DELETE` | `/api/disks/{id}` | Delete an unattached disk | +| `DELETE` | `/api/disks/{id}` | Delete an unattached disk; remote kouja deletes return `202` and finish asynchronously | | `GET` | `/api/media` | List installation media | | `POST` | `/api/media` | Register an existing ISO file | | `GET` | `/api/media/{id}` | Get one media entry | @@ -332,8 +332,12 @@ curl -sS -X POST http://127.0.0.1:8080/api/disks \ ``` `POST /api/disks` accepts an optional `storage_backend_id`; omit it for the -default backend. Empty disks run `lvcreate` against the selected backend's thin -pool. A backend with `supports_templates` can also create a disk from another +default backend. LVM disks run `lvcreate` against the selected backend's thin +pool. kouja disks are created and exported by the kouja agent; their response +includes `source=kouja`, `protocol=iscsi`, a display-only `locator`, and empty +`device`. Delete a kouja disk with `DELETE /api/disks/{id}`; the API returns +`202 Accepted` and the reclaimer unexports and deletes the volume. A +`delete_failed` disk can be retried with the same DELETE call. A backend with `supports_templates` can also create a disk from another disk marked as a template using `template_disk_id`; the backend's `preparation_script` creates the clone LV/device. The clone size defaults to the template size and may only be increased. diff --git a/docs/architecture.md b/docs/architecture.md index eecfa62..fed44d8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,7 +1,7 @@ # Architecture Rookery is a single-host management plane for KVM virtual machines backed by -LVM thin storage and FD.io VPP networking. It is split into two long-running Go +LVM thin, kouja, and host-device storage plus FD.io VPP networking. It is split into two long-running Go processes: - `rookery` owns the API, SQLite, QEMU lifecycle, LVM, and VPP network diff --git a/docs/operations.md b/docs/operations.md index 84f2530..868f64b 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -186,7 +186,13 @@ stopped. The `default` backend is bootstrapped from the API `-vg`/`-thin-pool` flags when supplied; otherwise `serve` starts with the first registered storage backend, and adding a backend in the UI updates -readiness. LVM thin-pool backends can be registered and selected from the UI. +readiness. LVM thin-pool backends can be registered and selected from the UI. Kouja +backends are registered with the driver `kouja`, an endpoint, storage class, +iSCSI initiator IQN, and mTLS certificate paths. The client certificate must +contain exactly one `spiffe://kouja/caller/` URI SAN; keep that identity +stable across certificate rotation. Kouja disks are exported directly to QEMU; +the host does not create an `iscsiadm` session. Queue a kouja disk delete and +let the reclaimer finish it; `delete_failed` rows can be retried. Each configured LVM backend has an **Import** action that lists existing thin volumes in its pool and registers selected volumes as disks without modifying LVM. Already registered volumes are shown as unavailable. The driver and config diff --git a/internal/webui/static/build/app.js b/internal/webui/static/build/app.js index 4d5532e..3fae4d4 100644 --- a/internal/webui/static/build/app.js +++ b/internal/webui/static/build/app.js @@ -1,52 +1,54 @@ -var Pe,ns,$o,at,Qn,Et,ss,Tt,as,tn,en,Ro,Lt={},It=[],Fo=/^m(i|n|o|s|text|space)$/,nn=Array.isArray,sn=It.slice,lt=Object.assign;function At(t){t&&t.parentNode&&t.remove()}function os(t,n,s){var l,a,o,f={},r=arguments.length;for(o in n)o=="key"?l=n[o]:o=="ref"&&typeof t!="function"?a=n[o]:f[o]=n[o];return r>2&&(f.children=r>3?sn.call(arguments,2):s),ht(t,f,l,a,null)}function ht(t,n,s,l,a){var o={type:t,props:n,key:s,ref:l,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:a||++ns,__i:-1,__u:0};return!a&&Pe.vnode&&Pe.vnode(o),o}function ge(t){return t.children}function Pt(t,n){this.props=t,this.context=n,this.__g=0}function ot(t,n){if(n==null)return t.__?ot(t.__,t.__i+1):null;for(var s;nn&&at.sort(ss),t=at.shift(),n=at.length,8&t.__g&&(l=void 0,a=void 0,o=(a=(s=t).__v).__e,f=[],r=[],(b=s.__P)&&((l=lt({constructor:void 0},a)).__v=a.__v+1,Pe.vnode&&Pe.vnode(l),an(b,l,a,s.__n,b.namespaceURI,32&a.__u?[o]:null,f,o||ot(a),32&a.__u,r),l.__v=a.__v,l.__.__k[l.__i]=l,cs(f,l,r),a.__=a.__e=null,l.__e!=o&&is(l)))}finally{at.length=Et=0}}function rs(t,n,s,l,a,o,f,r,b,w,u){var k,h,A,I,m,g,P=l.__k||It,L=n.length;for(b=Ho(s,n,P,b,L),k=0;kb?P--:P++,L=!0)):(~w||(a>m?P--:a>1](u?1:0)){for(a=s-1,o=s+1;a>=0||o=0?a--:o++])&&!(2&w.__u)&&r==w.key&&b==w.type)return f}return-1}function ts(t,n,s){s==null&&(s=""),n[0]=="-"?t.setProperty(n,s):t[n]=s}function St(t,n,s,l,a){var o,f;e:if(n=="style")if(typeof s=="string")t.style.cssText=s;else{if(typeof l=="string"&&(t.style.cssText=l=""),l)for(n in l)s&&n in s||ts(t.style,n,"");if(s)for(n in s)l&&s[n]==l[n]||ts(t.style,n,s[n])}else if(n[0]=="o"&&n[1]=="n")(t.__e||(t.__e={}))[n]=s,s&&l||(f=en[n]||(en[n]=Uo(n)),(t.__a||(t.__a={}))[n]=tn,o=n!=(n=n.replace(as,"$1")),(n=n.slice(2))[0]<"a"&&(n=n.toLowerCase()),s?t.addEventListener(n,f,o):t.removeEventListener(n,f,o));else{if(a=="http://www.w3.org/2000/svg")n=n.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(n!="width"&&n!="height"&&n!="href"&&n!="list"&&n!="form"&&n!="tabIndex"&&n!="download"&&n!="rowSpan"&&n!="colSpan"&&n!="role"&&n!="popover"&&n in t)try{t[n]=s??"";break e}catch{}typeof s=="function"||(s==null||s===!1&&n[4]!="-"?t.removeAttribute(n):t.setAttribute(n,n=="popover"&&s==1?"":s))}}function Uo(t){return function(n){if(this.__e){var s=this.__e[t];if(n[Tt]==null)n[Tt]=tn++;else if(n[Tt]=s.__.length&&s.__.push({}),s.__[t]}function d(t){return Nt=1,zo(ks,t)}function zo(t,n,s){var l=pn(gt++,2);if(l.t=t,!l.__c&&(l.__=[s?s(n):ks(void 0,n),function(o){var f=l.__N?l.__N[0]:l.__[0],r=l.t(f,o);dn(f,r)||(l.__N=[r,l.__[1]],l.__c.setState({}))}],l.__c=Re,!Re.__f)){Re.__f=!0;var a=Re.shouldComponentUpdate;Re.shouldComponentUpdate=function(o,f,r){var b=this.__H;if(!b)return!0;var w=!1,u=this.props!=o;if(b.__.some(function(h){h.__N&&(w=!0,dn(h.__[0],h.__N[0])||(u=!0))}),a){var k=a.call(this,o,f,r);return w?k||u:k}return!w||u}}return l.__}function z(t,n){var s=pn(gt++,3);!Me.__s&&ws(s.__H,n)&&(s.__P=!0,s.__=t,s.u=n,Re.__H.__h.push(s))}function te(t){return Nt=5,We(function(){return{current:t}},[])}function We(t,n){var s=pn(gt++,7);return ws(s.__H,n)&&(s.__=t(),s.__H=n),s.__}function Go(){var t;do{for(;t=cn.shift();)try{bt(t)}catch(s){Me.__e(s,{__:(t=t.__P)&&t.__v})}for(;t=_s.shift();){var n=t.__H;if(t.__P&&n)try{n.__h.some(bt),n.__h.some(un),n.__h=[]}catch(s){n.__h=[],Me.__e(s,t.__v)}}}while(cn.length)}Me.__b=function(t){Re=null,ps&&ps(t)},Me.__=function(t,n){t&&n.__k&&n.__k.__m&&(t.__m=n.__k.__m),gs&&gs(t,n)},Me.__r=function(t){fs&&fs(t),gt=0;var n=(Re=t.__c).__H;n&&(ln==Re?Re.__h=[]:(n.__h.some(bt),n.__h.some(un),gt=0),n.__h=[],n.__.some(function(s){s.__N&&(s.__=s.__N),s.u=s.__N=void 0})),ln=Re},Me.diffed=function(t){ms&&ms(t);var n=t.__c;n&&n.__H&&(n.__H.__h.length&&ys(_s.push(n)),n.__H.__.some(function(s){s.u&&(s.__H=s.u)})),ln=Re=null},Me.__c=function(t,n){n.some(function(s){try{s.__h.some(bt),s.__h=s.__h.filter(function(l){return!l.__||un(l)})}catch(l){n.some(function(a){a.__h&&(a.__h=[])}),n=[],Me.__e(l,s.__v)}}),hs&&hs(t,n)},Me.unmount=function(t){bs&&bs(t);var n,s,l=t.__c;l&&l.__H&&(l.__H.__.some(function(a){try{if(a.__P&&a.__c){if(s===void 0){for(s=t.__;s&&(!s.__c||!s.__c.__P);)s=s.__;s=s&&s.__c}a.__P=s,ys(cn.push(a))}else bt(a)}catch(o){n=o}}),l.__H=void 0,n&&Me.__e(n,l.__v))};var vs=typeof requestAnimationFrame=="function";function Ko(t){var n,s=function(){clearTimeout(l),vs&&cancelAnimationFrame(n),setTimeout(t)},l=setTimeout(s,35);vs&&(n=requestAnimationFrame(s))}function ys(t){t!=1&&us==Me.requestAnimationFrame||((us=Me.requestAnimationFrame)||Ko)(Go)}function bt(t){var n=Re,s=t.__c;typeof s=="function"&&(t.__c=void 0,s()),Re=n}function un(t){var n=Re;t.__c=t.__(),Re=n}function ws(t,n){return!t||t.length!=n.length||n.some(function(s,l){return!dn(s,t[l])})}function ks(t,n){return typeof n=="function"?n(t):n}var Jo=0;function e(t,n,s,l,a,o){n||(n={});var f,r,b=n;if("ref"in b&&typeof t!="function")for(r in b={},n)r=="ref"?f=n[r]:b[r]=n[r];var w={type:t,props:b,key:s,ref:f,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--Jo,__i:-1,__u:0};return(a||o)&&(w.__source=a,w.__self=o),Pe.vnode&&Pe.vnode(w),w}var xt=null,Dt=[],Xo=1;function oe(t,n=!1){let s={id:Xo++,message:t,error:n};xt?xt(s):Dt.push(s)}function Zo(){let[t,n]=d(null),s=te(null);return z(()=>{let l=a=>{s.current!==null&&window.clearTimeout(s.current),n(a),s.current=window.setTimeout(()=>{n(null),s.current=null},3500)};if(xt=l,Dt.length>0){let a=Dt;Dt=[];for(let o of a)l(o)}return()=>{xt=null,s.current!==null&&(window.clearTimeout(s.current),s.current=null)}},[]),t?e("div",{class:t.error?"toast error":"toast",role:"status","aria-live":"polite",children:t.message}):null}function Cs(t){Fe(e(Zo,{}),t)}var Ze=class extends Error{constructor(n,s){super(n),this.name="APIError",this.status=s}},Ls=[1e3,2e3,4e3,1e4],Mt=Ls.length+1,jo=new Set([429,503]),Yo=new Set([408,425,429,500,502,503,504]),Ss=new Set(["GET","HEAD","PUT","DELETE","OPTIONS"]),Vt=new Map;function Is(t){return(t.method||"GET").toUpperCase()}function Ts(){return typeof DOMException=="function"?new DOMException("The request was aborted","AbortError"):Object.assign(new Error("The request was aborted"),{name:"AbortError"})}function Qo(t,n){return new Promise((s,l)=>{if(n?.aborted){l(Ts());return}let a=window.setTimeout(()=>{n?.removeEventListener("abort",o),s()},t),o=()=>{window.clearTimeout(a),l(Ts())};n?.addEventListener("abort",o,{once:!0})})}function ei(t,n,s){return s?.aborted?!1:n.status===void 0?Ss.has(t):jo.has(n.status)?!0:Ss.has(t)&&Yo.has(n.status)}function Ps(t,n,s){let l=s.status===void 0?s.message||"network error":`${s.message||"request failed"} (HTTP ${s.status})`;return new Ze(`${t} ${n} failed after ${Mt} attempts: ${l}`,s.status)}async function ti(t,n){let s=new Headers(n.headers||{});n.body!==void 0&&s.set("Content-Type","application/json");let l;try{l=await fetch(t,{...n,headers:s})}catch(o){throw o instanceof Error&&o.name==="AbortError"?o:new Ze(o instanceof Error?o.message:String(o))}let a=null;if(l.status!==204){let o=await l.text();if(o)try{a=JSON.parse(o)}catch{a=null}}if(!l.ok){let o=`Request failed (${l.status})`;throw a&&typeof a=="object"&&("error"in a&&typeof a.error=="string"?o=a.error:"message"in a&&typeof a.message=="string"&&(o=a.message)),new Ze(o,l.status)}return a}async function Es(t,n){let s=Is(n),l;for(let a=1;a<=Mt;a+=1)try{return await ti(t,n)}catch(o){if(!(o instanceof Ze)||(l=o,!ei(s,o,n.signal)))throw o;if(a>=Mt)throw Ps(s,t,o);let f=Ls[a-1];console.warn(`${s} ${t} failed (attempt ${a}/${Mt}); retrying in ${f/1e3}s: ${o.message}`),await Qo(f,n.signal)}throw Ps(s,t,l??new Ze("request failed"))}function V(t,n={}){if(Is(n)!=="GET"||n.signal)return Es(t,n);let l=`GET ${t}`,a=Vt.get(l);if(a)return a;let o=Es(t,n).finally(()=>{Vt.get(l)===o&&Vt.delete(l)});return Vt.set(l,o),o}function ni(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function fn(t,n){let s=new Blob([n],{type:"application/x-pem-file"}),l=URL.createObjectURL(s),a=document.createElement("a");a.href=l,a.download=t,a.click(),URL.revokeObjectURL(l)}function As(){let t=te(null),[n,s]=d(!1),[l,a]=d(!0),[o,f]=d([]),[r,b]=d(""),[w,u]=d("365"),[k,h]=d(null),[A,I]=d(""),[m,g]=d(!1),[P,L]=d(!1);z(()=>{if(!n){t.current?.close();return}t.current?.showModal(),h(null),I(""),S()},[n]);let S=async()=>{g(!0);try{let T=await fetch("/api/pki/clients",{headers:{Accept:"application/json"}}),H=await T.text(),G=null;if(H)try{G=JSON.parse(H)}catch{G=null}if(!T.ok){let J=G&&typeof G=="object"&&"error"in G&&typeof G.error=="string"?G.error:`Request failed (${T.status})`;throw new Ze(J,T.status)}f(Array.isArray(G)?G:[]),a(!0),I("")}catch(T){T instanceof Ze&&T.status===503?(a(!1),I("mTLS is not enabled on this server. Start rookery with -mtls-addr and -pki-dir.")):I(T instanceof Error?T.message:String(T))}finally{g(!1)}},N=async T=>{if(T.preventDefault(),!(P||!r.trim())){L(!0),I("");try{let H=await V("/api/pki/clients",{method:"POST",body:JSON.stringify({name:r.trim(),days:Number(w)||365})});h(H),b(""),await S(),oe(`Issued client certificate for ${H.name}`)}catch(H){I(H instanceof Error?H.message:String(H))}finally{L(!1)}}},p=async T=>{if(window.confirm(`Revoke the certificate for ${T.name}? Existing connections will fail on the next handshake.`)){L(!0),I("");try{await V(`/api/pki/clients/${T.id}`,{method:"DELETE"}),await S(),oe(`Revoked client certificate for ${T.name}`)}catch(H){I(H instanceof Error?H.message:String(H))}finally{L(!1)}}};return e("div",{class:"sidebar-settings",children:[e("p",{class:"eyebrow",children:"Access"}),e("button",{class:"tab",type:"button",onClick:()=>s(!0),children:"Client certificates"}),e("span",{class:"field-hint",children:"Public mTLS API access"}),e("dialog",{ref:t,class:"dialog dialog-wide","aria-labelledby":"client-certs-title",onClose:()=>s(!1),onClick:T=>{T.target===T.currentTarget&&s(!1)},onCancel:T=>{P&&T.preventDefault()},children:e("div",{class:"dialog-body",children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"mTLS access"}),e("h2",{id:"client-certs-title",children:"Client certificates"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>s(!1),children:"\xD7"})]}),l?e(ge,{children:[e("p",{class:"field-hint",children:"Clients connect to the public mTLS listener and must present a certificate signed by this server's CA. Private keys are shown once and never stored by Rookery."}),e("div",{class:"actions",children:e("a",{class:"button ghost",href:"/api/pki/ca",download:"rookery-ca.pem",children:"Download CA certificate"})}),e("form",{class:"form-grid",onSubmit:T=>{N(T)},children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Client name"}),e("input",{type:"text",required:!0,maxlength:64,autocomplete:"off",spellcheck:!1,placeholder:"ops-laptop",value:r,onInput:T=>b(T.currentTarget.value)}),e("span",{class:"field-hint",children:"Used as the certificate common name."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Validity (days)"}),e("input",{type:"number",required:!0,min:1,max:3650,value:w,onInput:T=>u(T.currentTarget.value)})]}),e("div",{class:"field field-wide actions",children:e("button",{class:"button primary",type:"submit",disabled:P||!r.trim(),children:P?"Issuing\u2026":"Issue certificate"})})]})]}):e("div",{class:"banner",role:"alert",children:A||"mTLS is not enabled on this server."}),k?e("section",{class:"panel panel-spaced","aria-labelledby":"issued-cert-title",children:[e("div",{class:"panel-head",children:e("div",{children:[e("h3",{id:"issued-cert-title",children:["Certificate for ",k.name]}),e("p",{class:"panel-subtitle",children:"Download or copy these files now; the private key will not be shown again."})]})}),e("div",{class:"actions",children:[e("button",{class:"button primary",type:"button",onClick:()=>fn("client.crt",k.certificate_pem),children:"Download client.crt"}),e("button",{class:"button primary",type:"button",onClick:()=>fn("client.key",k.private_key_pem),children:"Download client.key"}),e("button",{class:"button ghost",type:"button",onClick:()=>fn("ca.crt",k.ca_pem),children:"Download ca.crt"})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Client certificate (PEM)"}),e("textarea",{class:"cert-pem",readOnly:!0,rows:5,value:k.certificate_pem})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Client private key (PEM)"}),e("textarea",{class:"cert-pem",readOnly:!0,rows:5,value:k.private_key_pem})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"CA certificate (PEM)"}),e("textarea",{class:"cert-pem",readOnly:!0,rows:4,value:k.ca_pem})]})]}):null,A&&l?e("p",{class:"form-error",role:"alert",children:A}):null,l?e("div",{class:"table-wrap",children:m?e("p",{class:"field-hint",children:"Loading issued certificates\u2026"}):o.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Serial"}),e("th",{scope:"col",children:"Expires"}),e("th",{scope:"col",children:"Status"}),e("th",{scope:"col"})]})}),e("tbody",{children:o.map(T=>e("tr",{children:[e("td",{children:T.name}),e("td",{class:"mono",children:[T.serial.slice(0,16),"\u2026"]}),e("td",{children:ni(T.not_after)}),e("td",{children:e("span",{class:"status-badge","data-status":T.revoked?"disabled":"running",children:T.revoked?"revoked":"valid"})}),e("td",{children:T.revoked?null:e("button",{class:"row-action danger",type:"button",disabled:P,onClick:()=>{p(T)},children:"Revoke"})})]},T.id))})]}):e("p",{class:"field-hint",children:"No client certificates have been issued yet."})}):null,e("footer",{class:"dialog-actions",children:e("button",{class:"button ghost",type:"button",onClick:()=>s(!1),children:"Close"})})]})})]})}var vt="rookery:route-changed";function $t(){let t=window.location.pathname.replace(/\/+$/,"")||"/";return{path:t,segments:t.split("/").filter(Boolean),query:new URLSearchParams(window.location.search),search:window.location.search}}function Q(t,n=!1){let s=t.startsWith("/")?t:`/${t}`;`${window.location.pathname}${window.location.search}`!==s&&(n?window.history.replaceState(null,"",s):window.history.pushState(null,"",s),window.dispatchEvent(new CustomEvent(vt)))}function ze(){let[t,n]=d($t);return z(()=>{let s=()=>n($t());return window.addEventListener("popstate",s),window.addEventListener(vt,s),()=>{window.removeEventListener("popstate",s),window.removeEventListener(vt,s)}},[]),t}var je=["vms","disks","media","networks","vnfs","lightvm","settings"],si={vms:{eyebrow:"Thin-LVM inventory",title:"Virtual machines",lede:"Create instances, attach disks and installation media, start them with QEMU/KVM, and open their consoles."},disks:{eyebrow:"Thin-LVM inventory",title:"Disks",lede:"Create and manage instance disks separately, then attach an unattached disk to a VM."},media:{eyebrow:"Installation media",title:"Installation media",lede:"Register read-only ISO files from the host, then choose one as a VM's boot media."},networks:{eyebrow:"VPP bridge domains",title:"Network",lede:"Create VPP bridge domains and vhost-user interfaces, then attach them to stopped VMs."},vnfs:{eyebrow:"Network functions",title:"VNFs",lede:"Attach DHCPv4 and DHCPv6 functions to Rookery Networks, enable them, and inspect their leases."},lightvm:{eyebrow:"LightVM inventory",title:"LightVM",lede:"Upload kernels and initramfs images, then define container-backed microVMs."},settings:{eyebrow:"Rookery server",title:"Settings",lede:"Configure cgroup resource limits, server-wide Rookery behavior, and lifecycle hooks."}};function ye(t){let n=document.getElementById(t);if(!n)throw new Error(`missing UI element #${t}`);return n}function Ns(){let t=ye("api-status"),n=ye("api-status-label"),s=ye("hero-eyebrow"),l=ye("hero-title"),a=ye("hero-lede"),o=ye("readiness-summary"),f=ye("thin-pool-card"),r=ye("thin-pool-state"),b=ye("thin-pool-target"),w=ye("thin-pool-detail"),u=ye("vpp-card"),k=ye("vpp-state"),h=ye("vpp-target"),A=ye("vpp-detail"),I={card:ye("rookery-startup-script-card"),state:ye("rookery-startup-script-state"),target:ye("rookery-startup-script-target"),detail:ye("rookery-startup-script-detail")},m={card:ye("vpp-startup-script-card"),state:ye("vpp-startup-script-state"),target:ye("vpp-startup-script-target"),detail:ye("vpp-startup-script-detail")},g={vms:ye("tab-vms"),disks:ye("tab-disks"),media:ye("tab-media"),networks:ye("tab-networks"),vnfs:ye("tab-vnfs"),lightvm:ye("tab-lightvm"),settings:ye("tab-settings")},P={vms:ye("vms-panel"),disks:ye("disks-panel"),media:ye("media-panel"),networks:ye("networks-panel"),vnfs:ye("vnfs-panel"),lightvm:ye("lightvm-panel"),settings:ye("settings-panel")},L={vms:ye("new-vm"),disks:ye("new-disk"),media:ye("new-media"),networks:ye("new-network")},S="vms",N={ready:"ready",checking:"checking",missing:"missing",not_configured:"not configured",unreachable:"unreachable",error:"error"},p=E=>{let M={online:"api online",offline:"api offline",pending:"connecting"};t.dataset.state=E,n.textContent=M[E]},T=(E,M,se,ce,v,$)=>{let C=v?.state||"checking";E.dataset.state=C,M.textContent=N[C]||C,se.textContent=$,se.title=$;let U=v?.detail||"";!U&&C==="checking"&&(U="Checking\u2026"),ce.textContent=U},H=E=>{if(!E)return"";let M=new Date(E);return Number.isNaN(M.getTime())?"":M.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})},G=(E,M,se)=>{let ce="unreachable",v="unavailable",$="status unavailable",C="Could not read the startup script status.";if(M){let U=M.status||(M.enabled?"pending":"disabled"),de=H(M.ran_at);switch(U){case"success":ce="ready",v="success",$=de?`last run ${de}`:"last run succeeded",C=`exit code ${M.exit_code??0}`;break;case"failed":ce="error",v="failed",$=de?`last run ${de}`:"last run failed",C=`exit code ${M.exit_code??"\u2014"} \xB7 open Settings for output`;break;case"running":ce="checking",v="running",$="running now",C=de?`started ${de}`:"Waiting for completion\u2026";break;case"pending":ce="missing",v="pending",$="not run yet",C=se==="vpp"?"Runs on the next VPP connection":"Runs on the next API start";break;case"disabled":ce="not_configured",v="disabled",$="disabled",C="Enable it in Settings";break;case"unavailable":ce="unreachable",v="unavailable",$="status unavailable",C="Could not read the startup script status.";break}}E.card.dataset.state=ce,E.state.textContent=v,E.target.textContent=$,E.target.title=$,E.detail.textContent=C},J=E=>{let M=!!E.ready,se=E.thin_pool,ce=E.vpp,v="not configured";if(se?.vg&&se.name?v=`${se.vg}/${se.name}`:se?.vg&&(v=se.vg),T(f,r,b,w,se,v),T(u,k,h,A,ce,ce?.socket||"not configured"),G(I,E.rookery_startup_script,"rookery"),G(m,E.vpp_startup_script,"vpp"),M)o.dataset.state="ready",o.textContent="ready for VMs";else{let U=[se?.state,ce?.state].filter(Boolean);U.includes("unreachable")||U.includes("error")?o.dataset.state="unreachable":U.includes("missing")||U.includes("not_configured")?o.dataset.state="missing":o.dataset.state="checking",o.textContent=o.dataset.state==="checking"?"checking":"VM creation blocked"}let $=!!se?.ready,C=!!ce?.ready;window.dispatchEvent(new CustomEvent("rookery:readiness-changed",{detail:{ready:M,thinPool:$}})),window.dispatchEvent(new CustomEvent("rookery:vpp-ready",{detail:C}))},ne=()=>{T(f,r,b,w,{state:"unreachable",detail:"Cannot reach the API."},"status unavailable"),T(u,k,h,A,{state:"unreachable",detail:"Cannot reach the API."},"status unavailable"),G(I,void 0,"rookery"),G(m,void 0,"vpp"),o.dataset.state="unreachable",o.textContent="status unavailable",window.dispatchEvent(new CustomEvent("rookery:readiness-changed",{detail:{ready:!1,thinPool:!1}})),window.dispatchEvent(new CustomEvent("rookery:vpp-ready",{detail:!1}))},x=async()=>{try{let E=await V("/api/status");J(E),p("online")}catch{p("offline"),ne()}},R=E=>je.includes(E),F=E=>{let M=E.split("/").filter(Boolean)[0]||"vms";return M==="storage-backends"?"disks":M==="interfaces"||M==="acls"||M==="route-tables"?"networks":R(M)?M:"vms"},W=(E,M=!1,se=!0)=>{let ce=R(E)?E:"vms",v=ce!==S;S=ce;for(let C of je){let U=C===ce;g[C].setAttribute("aria-selected",String(U)),g[C].tabIndex=U?0:-1,P[C].hidden=!U;let de=L[C];de&&(de.hidden=!U)}se&&Q(`/${ce}`);let $=si[ce];s.textContent=$.eyebrow,l.textContent=$.title,a.textContent=$.lede,document.title=`${$.title} \xB7 rookery`,v&&window.dispatchEvent(new CustomEvent("rookery:tab-changed",{detail:ce})),M&&g[ce].focus()},Y=E=>{if(!["ArrowLeft","ArrowRight","Home","End"].includes(E.key))return;E.preventDefault();let se=je.indexOf(S);E.key==="Home"?W(je[0],!0):E.key==="End"?W(je[je.length-1],!0):E.key==="ArrowLeft"?W(je[(se-1+je.length)%je.length],!0):W(je[(se+1)%je.length],!0)},y=()=>{let E=$t();E.path==="/"&&window.history.replaceState(null,"","/vms");let M=E.path.startsWith("/route-tables")||E.path==="/vnfs/new"||/^\/vnfs\/[^/]+\/edit$/.test(E.path);document.body.classList.toggle("route-page-mode",M),W(F(E.path),!1,!1)};for(let E of je)g[E].addEventListener("click",()=>W(E)),g[E].addEventListener("keydown",Y);L.vms?.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("rookery:add-vm"))}),L.disks?.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("rookery:add-disk"))}),L.media?.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("rookery:add-media"))}),L.networks?.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("rookery:add-network"))}),window.addEventListener("popstate",y),window.addEventListener(vt,y),window.addEventListener("rookery:open-tab",E=>{let M=String(E.detail||"");R(M)&&W(M)}),window.setInterval(()=>{document.hidden||x()},1e4);let ae=window.location.hash.replace(/^#/,"");ae&&R(ae)&&window.history.replaceState(null,"",`/${ae}`),y(),x()}var Rt=64;function Ne(t){let[n,s]=d({}),l=async()=>{try{let o=await V(`/api/labels/${encodeURIComponent(t)}`),f={};for(let r of Array.isArray(o)?o:[])f[r.entity_id]=Array.isArray(r.labels)?r.labels:[];s(f)}catch{s({})}};return z(()=>{l()},[t]),z(()=>{let o=f=>{let r=f.detail;!r||r.entityType!==t||!r.entityID||s(b=>({...b,[r.entityID]:Array.isArray(r.labels)?r.labels:[]}))};return window.addEventListener("rookery:labels-changed",o),()=>window.removeEventListener("rookery:labels-changed",o)},[t]),{labels:n,reload:l,setEntityLabels:(o,f)=>{s(r=>({...r,[o]:f}))}}}function ke(t,n){let[s,l]=d([]);return z(()=>{if(n==null||String(n)===""){l([]);return}let a=!0;return V(`/api/labels/${encodeURIComponent(t)}/${encodeURIComponent(String(n))}`).then(o=>{a&&l(Array.isArray(o.labels)?o.labels:[])}).catch(()=>{a&&l([])}),()=>{a=!1}},[t,n]),[s,l]}async function Ds(t,n,s){let l=await V(`/api/labels/${encodeURIComponent(t)}/${encodeURIComponent(String(n))}`,{method:"PUT",body:JSON.stringify({labels:s})}),a=Array.isArray(l.labels)?l.labels:[];return window.dispatchEvent(new CustomEvent("rookery:labels-changed",{detail:{entityType:t,entityID:String(n),labels:a}})),a}async function Ce(t,n,s,l){try{await Ds(t,n,l)}catch(a){oe(`Saved ${s}, but labels could not be updated: ${a instanceof Error?a.message:String(a)}`,!0)}}function Se({labels:t,onChange:n,disabled:s=!1}){let[l,a]=d(""),o=Array.isArray(t)?t:[],f=()=>{let r=l.split(/[\n,]+/).map(w=>w.trim()).filter(Boolean);if(r.length===0)return;let b=[...o];for(let w of r)b.includes(w)||b.push(w);n(b),a("")};return e("div",{class:"labels-field",children:[e("span",{class:"field-label",children:"Labels"}),e("div",{class:"labels-editor-list",children:o.length?o.map(r=>e("span",{class:"label-chip editable",children:[r,e("button",{type:"button","aria-label":`Remove ${r}`,disabled:s,onClick:()=>n(o.filter(b=>b!==r)),children:"\xD7"})]},r)):e("span",{class:"field-hint",children:"No labels yet."})}),e("div",{class:"labels-editor-input",children:[e("input",{type:"text",value:l,maxLength:256,disabled:s||o.length>=Rt,placeholder:"production, web, critical",onInput:r=>a(r.currentTarget.value),onKeyDown:r=>{r.key==="Enter"&&(r.preventDefault(),f())}}),e("button",{class:"button ghost",type:"button",disabled:s||!l.trim()||o.length>=Rt,onClick:f,children:"Add"})]}),e("span",{class:"field-hint",children:["Add as many labels as you need. Commas separate multiple labels; up to ",Rt,"."]})]})}function De({entityType:t,entityID:n,name:s,labels:l,onSaved:a}){let[o,f]=d(!1),r=Array.isArray(l)?l:[];return e("div",{class:"entity-labels",children:[r.map(b=>e("span",{class:"label-chip",title:b,children:b},b)),e("button",{class:"label-add",type:"button","aria-label":`Edit labels for ${s}`,onClick:()=>f(!0),children:r.length?"edit labels":"+ label"}),o?e(ai,{entityType:t,entityID:String(n),name:s,labels:r,onClose:()=>f(!1),onSaved:b=>{a(b),f(!1)}}):null]})}function ai({entityType:t,entityID:n,name:s,labels:l,onClose:a,onSaved:o}){let f=te(null),r=te(null),[b,w]=d(l),[u,k]=d(""),[h,A]=d(""),[I,m]=d(!1);z(()=>{f.current?.showModal(),r.current?.focus()},[]);let g=()=>{I||f.current?.close()},P=()=>{let S=u.split(/[\n,]+/).map(N=>N.trim()).filter(Boolean);S.length!==0&&(w(N=>{let p=[...N];for(let T of S)p.includes(T)||p.push(T);return p}),k(""),A(""))},L=async S=>{if(S.preventDefault(),!I){m(!0),A("");try{let N=await Ds(t,n,b);o(N),oe(`Saved labels for ${s}`)}catch(N){A(N instanceof Error?N.message:String(N))}finally{m(!1)}}};return e("dialog",{ref:f,class:"dialog labels-dialog","aria-labelledby":"labels-dialog-title",onClose:a,onClick:S=>{S.target===S.currentTarget&&g()},onCancel:S=>{I&&S.preventDefault()},children:e("form",{onSubmit:S=>{L(S)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Labels"}),e("h2",{id:"labels-dialog-title",children:s})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:g,children:"\xD7"})]}),e("div",{class:"labels-editor",children:[e("span",{class:"field-hint",children:["Add as many labels as you need. Labels are free-form text; commas separate multiple labels. Up to ",Rt," per ",t,"."]}),e("div",{class:"labels-editor-list",children:b.length?b.map(S=>e("span",{class:"label-chip editable",children:[S,e("button",{type:"button","aria-label":`Remove ${S}`,disabled:I,onClick:()=>w(N=>N.filter(p=>p!==S)),children:"\xD7"})]},S)):e("span",{class:"field-hint",children:"No labels yet."})}),e("div",{class:"labels-editor-input",children:[e("input",{ref:r,type:"text",value:u,maxLength:256,placeholder:"production, web, critical",onInput:S=>k(S.currentTarget.value),onKeyDown:S=>{S.key==="Enter"&&(S.preventDefault(),P())}}),e("button",{class:"button ghost",type:"button",disabled:!u.trim()||I,onClick:P,children:"Add"})]}),h?e("p",{class:"form-error",role:"alert",children:h}):null]}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:I,onClick:g,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:I,children:I?"Saving\u2026":"Save labels"})]})]})})}var oi="[A-Za-z0-9][A-Za-z0-9._\\-]*";function Ms({backends:t,disks:n,onClose:s,onSaved:l}){let a=t.find(_=>_.name==="default")||t[0],[o,f]=d(t.length===0?"host":"lvm"),[r,b]=d(""),[w,u]=d("10"),[k,h]=d(a?String(a.id):""),[A,I]=d([]),[m,g]=d(""),[P,L]=d(!1),[S,N]=d(""),[p,T]=d("empty"),[H,G]=d(""),[J,ne]=d(!1),[x,R]=d(""),[F,W]=d("5m"),[Y,y]=d("{}"),[ae,E]=ke("disk"),[M,se]=d(!1),[ce,v]=d(""),$=te(null),C=te(null),U=te(null);z(()=>{$.current?.showModal(),C.current?.focus()},[]);let ie=!!(t.find(_=>String(_.id)===k)||null)?.config?.supports_templates,pe=n.filter(_=>_.is_template&&String(_.storage_backend_id??"")===k),ee=pe.find(_=>String(_.id)===H)||null,ue=A.find(_=>_.path===m)||null;z(()=>{ie||(T("empty"),G(""),ne(!1))},[ie]),z(()=>{if(o!=="host")return;let _=!1;return L(!0),N(""),V("/api/host/block-devices/attachable").then(B=>{_||I(Array.isArray(B)?B:[])}).catch(B=>{_||(I([]),N(B instanceof Error?B.message:String(B)))}).finally(()=>{_||L(!1)}),()=>{_=!0}},[o]);let me=()=>{M||$.current?.close()},c=async _=>{if(_.preventDefault(),M||!U.current?.reportValidity())return;if(o==="host"&&(!ue||!ue.available)){v("Select an available host block device.");return}let B;if(o==="lvm"&&p==="template"&&ee?.provisioning_script){try{B=JSON.parse(Y||"{}")}catch{v("Provisioning properties must be valid JSON.");return}if(B===null||Array.isArray(B)||typeof B!="object"){v("Provisioning properties must be a JSON object.");return}}v(""),se(!0);try{let X=o==="host"?{name:r.trim(),host_device:ue?.path}:{name:r.trim(),size_gb:Number(w),storage_backend_id:Number(k),...p==="template"&&ee?{template_disk_id:ee.id}:{},...p==="template"&&ee?.provisioning_script&&B!==void 0?{provisioning:B}:{},...p==="empty"&&ie?{is_template:J,...J?{provisioning_script:x.trim(),provisioning_timeout:F.trim()}:{}}:{}},Z=await V("/api/disks",{method:"POST",body:JSON.stringify(X)});await Ce("disk",Z.id,Z.name,ae),l(Z)}catch(X){v(X instanceof Error?X.message:String(X))}finally{se(!1)}};return e("dialog",{ref:$,id:"disk-dialog","aria-labelledby":"disk-dialog-title",onClose:s,onClick:_=>{_.target===_.currentTarget&&me()},onCancel:_=>{M&&_.preventDefault()},children:e("form",{ref:U,novalidate:!0,onSubmit:_=>{c(_)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"New disk"}),e("h2",{id:"disk-dialog-title",children:"Create disk"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:me,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:C,type:"text",required:!0,maxlength:127,pattern:oi,autocomplete:"off",spellcheck:!1,placeholder:"vm01-root",value:r,onInput:_=>b(_.currentTarget.value)}),e("span",{class:"field-hint",children:"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Storage source"}),e("select",{value:o,onChange:_=>f(_.currentTarget.value),children:[e("option",{value:"lvm",disabled:t.length===0,children:"LVM thin volume"}),e("option",{value:"host",children:"Host block device (passthrough)"})]}),e("span",{class:"field-hint",children:"Passthrough disks hand an existing host disk, partition, or logical volume to one VM. Rookery never deletes or resizes the underlying device."})]}),o==="lvm"?e(ge,{children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Size (GB)"}),e("input",{type:"number",required:!0,min:ee?ee.size_gb:1,max:65536,step:1,value:w,onInput:_=>u(_.currentTarget.value)}),e("span",{class:"field-hint",children:"Creates an LVM thin volume of this size in the configured pool."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Storage backend"}),e("select",{required:!0,value:k,onChange:_=>{h(_.currentTarget.value),G(""),T("empty")},children:t.map(_=>e("option",{value:String(_.id),children:[_.name," \xB7 ",_.driver==="lvm-thin"?"LVM thin":_.driver]},_.id))}),e("span",{class:"field-hint",children:ie?"Template cloning is available on this backend.":"Template cloning is disabled for this backend; enable it in Storage backends."})]}),ie?e("label",{class:"field",children:[e("span",{class:"field-label",children:"Creation source"}),e("select",{value:p,onChange:_=>T(_.currentTarget.value),children:[e("option",{value:"empty",children:"Empty disk"}),e("option",{value:"template",disabled:pe.length===0,children:["From template",pe.length===0?" (none available)":""]})]}),e("span",{class:"field-hint",children:"Template clones run the backend preparation script and the template's provisioning script when one is configured; empty disks use the thin pool directly."})]}):null,ie&&p==="template"?e("label",{class:"field",children:[e("span",{class:"field-label",children:"Template"}),e("select",{required:!0,value:H,onChange:_=>{let B=_.currentTarget.value;G(B);let X=pe.find(Z=>String(Z.id)===B);X&&u(String(X.size_gb))},children:[e("option",{value:"",children:"Select a template\u2026"}),pe.map(_=>e("option",{value:String(_.id),children:[_.name," \xB7 ",_.size_gb," GB",_.provisioning_script?" \xB7 provisioning":""]},_.id))]})]}):null,ie&&p==="empty"?e("label",{class:"field field-wide checkbox-field",children:e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:J,onChange:_=>ne(_.currentTarget.checked)}),e("span",{children:"Mark this new disk as a template"})]})}):null,ie&&p==="empty"&&J?e(ge,{children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"VM provisioning script"}),e("input",{type:"text",placeholder:"/usr/local/sbin/rookery-provision-template",value:x,onInput:_=>R(_.currentTarget.value)}),e("span",{class:"field-hint",children:["Optional absolute-path script run after a clone is created. It receives",e("code",{children:"ROOKERY_PROVISION_JSON"}),"."]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Provisioning timeout"}),e("input",{type:"text",placeholder:"5m",value:F,onInput:_=>W(_.currentTarget.value)})]})]}):null,ie&&p==="template"&&ee&&!ee.provisioning_script?e("p",{class:"field-hint field-wide",children:["This template has no provisioning script, so there are no key-values to pass. Close this dialog and use ",e("strong",{children:"Provisioning"})," on the template row to configure one first."]}):null,ie&&p==="template"&&ee?.provisioning_script?e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Provisioning properties (JSON object)"}),e("textarea",{rows:6,spellcheck:!1,value:Y,onInput:_=>y(_.currentTarget.value)}),e("span",{class:"field-hint",children:["Passed to the template provisioning script through",e("code",{children:"ROOKERY_PROVISION_JSON"})," under ",e("code",{children:"properties"}),". Set ",e("code",{children:'"resize_disk": true'})," to extend the root partition and filesystem with the reference script. Leave",e("code",{})," empty to skip provisioning."]})]}):null]}):e(ge,{children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Host block device"}),e("select",{required:!0,value:m,disabled:P,onChange:_=>{let B=_.currentTarget.value;g(B);let X=A.find(Z=>Z.path===B);X&&!r.trim()&&b(ii(X))},children:[e("option",{value:"",children:P?"Loading devices\u2026":"Select a host block device\u2026"}),A.map(_=>e("option",{value:_.path,disabled:!_.available,children:[xs(_.kind)," \xB7 ",_.path," \xB7 ",Vs(_.size_bytes),_.available?"":` \xB7 ${_.unavailable_reason||"unavailable"}`]},`${_.kind}:${_.path}`))]}),e("span",{class:"field-hint",children:"Whole disks, partitions, and logical volumes are listed. Mounted, swap, and otherwise busy devices are disabled."})]}),S?e("p",{class:"form-error field-wide",role:"alert",children:S}):null,ue?e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Selected device"}),e("p",{class:"field-hint",children:[xs(ue.kind)," \xB7 ",ue.path," \xB7"," ",Vs(ue.size_bytes),ue.model?` \xB7 ${ue.model}`:""]}),ue.available?null:e("p",{class:"form-error",role:"alert",children:ue.unavailable_reason||"This device is not available."})]}):null,A.length===0&&!P&&!S?e("p",{class:"field-hint field-wide",children:"No host block devices were found."}):null]}),e("div",{class:"field field-wide",children:e(Se,{labels:ae,onChange:E,disabled:M})})]}),ce?e("p",{class:"form-error",role:"alert",children:ce}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:M,onClick:me,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:M||(o==="lvm"?t.length===0||p==="template"&&!ee:!ue||!ue.available),children:M?"Creating\u2026":"Create disk"})]})]})})}function xs(t){switch(t){case"disk":return"Disk";case"partition":return"Partition";case"lv":return"Logical volume";case"raid":return"RAID array";default:return"Block device"}}function Vs(t){let n=Number(t)||0;return n>=1024**4?`${(n/1024**4).toFixed(n%1024**4===0?0:1)} TB`:n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function ii(t){return(t.path.split("/").filter(Boolean).pop()||t.name).replace(/[^A-Za-z0-9._-]+/g,"-").replace(/^[^A-Za-z0-9]+/,"").slice(0,127)||t.name}function $s({backend:t,onClose:n,onImported:s}){let[l,a]=d([]),[o,f]=d(()=>new Set),[r,b]=d(!0),[w,u]=d(!1),[k,h]=d(""),A=te(null),I=async()=>{b(!0),h("");try{let L=await V(`/api/storage-backends/${t.id}/importable-disks`);a(Array.isArray(L)?L:[]),f(new Set)}catch(L){a([]),h(L instanceof Error?L.message:String(L))}finally{b(!1)}};z(()=>{A.current?.showModal(),I()},[]);let m=()=>{w||A.current?.close()},g=(L,S)=>{f(N=>{let p=new Set(N);return S?p.add(L):p.delete(L),p})},P=async()=>{if(!(w||o.size===0)){u(!0),h("");try{let L=await V(`/api/storage-backends/${t.id}/import-disks`,{method:"POST",body:JSON.stringify({names:[...o]})});s(Array.isArray(L)?L:[])}catch(L){h(L instanceof Error?L.message:String(L))}finally{u(!1)}}};return e("dialog",{ref:A,id:"import-disk-dialog","aria-labelledby":"import-disk-dialog-title",onClose:n,onClick:L=>{L.target===L.currentTarget&&m()},onCancel:L=>{w&&L.preventDefault()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Storage backend"}),e("h2",{id:"import-disk-dialog-title",children:"Import existing disks"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:m,children:"\xD7"})]}),e("p",{class:"import-disk-lede",children:[t.name," \xB7 ",t.config.vg,"/",t.config.thin_pool," \u2014 select existing thin volumes to register as disks. Importing does not modify LVM, but deleting an imported disk later removes its volume."]}),k?e("div",{class:"banner",role:"alert",children:k}):null,e("div",{class:"import-disk-list",children:[r?e("p",{class:"import-disk-meta",children:"Loading existing thin volumes\u2026"}):null,!r&&l.length===0&&!k?e("div",{class:"empty import-disk-empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A3"}),e("h3",{children:"No thin volumes found"}),e("p",{children:"This storage backend has no existing thin volumes to import."})]}):null,r?null:l.map(L=>e("label",{class:L.registered?"import-disk-row imported":"import-disk-row",children:[e("input",{type:"checkbox",value:L.name,checked:o.has(L.name),disabled:L.registered||w,"aria-label":`Import ${L.name}`,onChange:S=>g(L.name,S.currentTarget.checked)}),e("span",{class:"import-disk-copy",children:[e("span",{class:"import-disk-name",children:L.name}),e("span",{class:"import-disk-meta",children:[L.size_gb," GB \xB7"," ",L.registered?L.attached_vm_name?`already imported \xB7 attached to ${L.attached_vm_name}`:L.storage_backend_name?`already imported \xB7 ${L.storage_backend_name}`:"already imported":"available to import"]})]}),e("span",{class:"import-disk-badge",children:L.registered?"imported":"new"})]},L.name))]}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:r||w,onClick:()=>{I()},children:r?"Refreshing\u2026":"Refresh"}),e("button",{class:"button ghost",type:"button",disabled:w,onClick:m,children:"Cancel"}),e("button",{class:"button primary",type:"button",disabled:w||o.size===0,onClick:()=>{P()},children:w?"Importing\u2026":o.size>0?`Import selected (${o.size})`:"Import selected"})]})]})}var ri="[A-Za-z0-9][A-Za-z0-9._\\-]*";function Rs({backend:t,onClose:n,onSaved:s}){let l=t!==null,[a,o]=d(t?.name??""),[f,r]=d(t?.title??""),[b,w]=d(t?.description??""),[u,k]=d(t?.driver??"lvm-thin"),[h,A]=d(t?.config?.vg??""),[I,m]=d(t?.config?.thin_pool??""),[g,P]=d(!!t?.config?.supports_templates),[L,S]=d(t?.config?.preparation_script??""),[N,p]=d(t?.config?.preparation_timeout||"5m"),[T,H]=d(!!t?.config?.allow_template_delete_with_clones),[G,J]=ke("storage_backend",t?.id),[ne,x]=d(!1),[R,F]=d(""),W=te(null),Y=te(null),y=te(null);z(()=>{W.current?.showModal(),Y.current?.focus()},[]);let ae=()=>{ne||W.current?.close()},E=async M=>{if(M.preventDefault(),!(ne||!y.current?.reportValidity())){F(""),x(!0);try{let se=await V(l?`/api/storage-backends/${t?.id}`:"/api/storage-backends",{method:l?"PUT":"POST",body:JSON.stringify({name:a.trim(),title:f.trim(),description:b.trim(),driver:u,config:{vg:h.trim(),thin_pool:I.trim(),supports_templates:g,preparation_script:g?L.trim():"",preparation_timeout:g?N.trim()||"5m":"",allow_template_delete_with_clones:g&&T}})});await Ce("storage_backend",se.id,se.name,G),s(se)}catch(se){F(se instanceof Error?se.message:String(se))}finally{x(!1)}}};return e("dialog",{ref:W,id:"storage-backend-dialog","aria-labelledby":"storage-backend-dialog-title",onClose:n,onClick:M=>{M.target===M.currentTarget&&ae()},onCancel:M=>{ne&&M.preventDefault()},children:e("form",{ref:y,novalidate:!0,onSubmit:M=>{E(M)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:l?"Edit backend":"New backend"}),e("h2",{id:"storage-backend-dialog-title",children:l?`Storage backend \xB7 ${t?.name}`:"Register storage backend"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:ae,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:Y,type:"text",required:!0,maxlength:64,pattern:ri,autocomplete:"off",spellcheck:!1,placeholder:"fast-pool",value:a,onInput:M=>o(M.currentTarget.value)}),e("span",{class:"field-hint",children:"Used when creating disks."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Title"}),e("input",{type:"text",maxlength:128,autocomplete:"off",placeholder:"Fast NVMe thin pool",value:f,onInput:M=>r(M.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Description ",e("em",{children:"optional"})]}),e("textarea",{rows:2,maxlength:2e3,placeholder:"What this backend is for",value:b,onInput:M=>w(M.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Driver"}),e("select",{required:!0,value:u,onChange:M=>k(M.currentTarget.value),children:e("option",{value:"lvm-thin",children:"LVM thin pool"})}),e("span",{class:"field-hint",children:"Additional drivers can be added later."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Volume group"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"vg0",value:h,onInput:M=>A(M.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Thin pool"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"rookery",value:I,onInput:M=>m(M.currentTarget.value)})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"field-label",children:"Templates"}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:g,onChange:M=>P(M.currentTarget.checked)}),e("span",{children:"Support template disks and template-based disk creation"})]}),e("span",{class:"field-hint",children:"Clone existing template disks with the backend preparation script."})]}),g?e(ge,{children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Preparation script"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"/usr/local/sbin/rookery-disk-prepare",value:L,onInput:M=>S(M.currentTarget.value)}),e("span",{class:"field-hint",children:"Absolute path. Runs as the API user with disk details in ROOKERY_DISK_JSON."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Script timeout"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"5m",value:N,onInput:M=>p(M.currentTarget.value)})]}),e("label",{class:"field field-wide checkbox-field",children:e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:T,onChange:M=>H(M.currentTarget.checked)}),e("span",{children:"Allow deleting a template while clones still exist"})]})})]}):null,e("div",{class:"field field-wide",children:e(Se,{labels:G,onChange:J,disabled:ne})})]}),R?e("p",{class:"form-error",role:"alert",children:R}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:ne,onClick:ae,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:ne,children:ne?"Saving\u2026":"Save backend"})]})]})})}function Fs({disk:t,onClose:n,onSaved:s}){let[l,a]=d(t.provisioning_script||""),[o,f]=d(t.provisioning_timeout||""),[r,b]=d(!1),[w,u]=d(""),k=te(null);z(()=>{k.current?.showModal()},[]);let h=()=>{r||k.current?.close()},A=async I=>{if(I.preventDefault(),!r){b(!0),u("");try{let m=await V(`/api/disks/${t.id}`,{method:"PUT",body:JSON.stringify({provisioning_script:l.trim(),provisioning_timeout:o.trim()})});s(m)}catch(m){u(m instanceof Error?m.message:String(m))}finally{b(!1)}}};return e("dialog",{ref:k,id:"template-provisioning-dialog","aria-labelledby":"template-provisioning-title",onClose:n,onClick:I=>{I.target===I.currentTarget&&h()},onCancel:I=>{r&&I.preventDefault()},children:e("form",{onSubmit:I=>{A(I)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Template provisioning"}),e("h2",{id:"template-provisioning-title",children:["Provisioning \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:h,children:"\xD7"})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Provisioning script"}),e("input",{type:"text",placeholder:"/usr/local/sbin/rookery-provision-template",value:l,onInput:I=>a(I.currentTarget.value)}),e("span",{class:"field-hint",children:["Absolute path to a script run after the backend clones this template. It receives the provisioning context and custom properties in ",e("code",{children:"ROOKERY_PROVISION_JSON"}),". Leave empty to disable provisioning."]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Timeout"}),e("input",{type:"text",placeholder:"5m",value:o,onInput:I=>f(I.currentTarget.value)}),e("span",{class:"field-hint",children:"Examples: 30s, 5m, 1h."})]}),w?e("p",{class:"form-error",role:"alert",children:w}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:r,onClick:h,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:r,children:r?"Saving\u2026":"Save provisioning"})]})]})})}var Bs=new Intl.NumberFormat;function Hs(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function li(t){let n=Number(t.size_bytes)||0;return n<=0?`${Bs.format(t.size_gb)} GB`:n>=1024**4?`${(n/1024**4).toFixed(n%1024**4===0?0:1)} TB`:n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:`${Bs.format(n)} B`}function Os(){let[t,n]=d([]),[s,l]=d([]),[a,o]=d(!0),[f,r]=d(""),[b,w]=d(""),[u,k]=d(!1),[h,A]=d(null),[I,m]=d(null),[g,P]=d(null),L=Ne("disk"),S=Ne("storage_backend"),N=ze();z(()=>{P(null)},[N.path,N.search]),z(()=>{let[y,ae,E]=N.segments;if(y==="disks"){k(ae==="new"),A(null),m(null);return}if(y==="storage-backends"){if(k(!1),ae==="new"){A({backend:null}),m(null);return}let M=s.find(se=>String(se.id)===ae);M?(A(E==="edit"?{backend:M}:null),m(E==="import"?M:null)):(A(null),m(null));return}k(!1),A(null),m(null)},[N.path,s]);let p=y=>{n(y),window.dispatchEvent(new CustomEvent("rookery:disks-changed",{detail:y}))},T=async(y=!0)=>{try{let ae=await V("/api/disks");p(Array.isArray(ae)?ae:[]),y&&r("")}catch(ae){y&&r(ae instanceof Error?ae.message:String(ae))}},H=async(y=!0)=>{try{let ae=await V("/api/storage-backends"),E=Array.isArray(ae)?ae:[];E.sort((M,se)=>M.name.localeCompare(se.name,void 0,{sensitivity:"base"})),l(E),y&&w("")}catch(ae){y&&w(ae instanceof Error?ae.message:String(ae))}},G=async()=>{o(!0),await Promise.allSettled([T(),H()]),o(!1)};z(()=>{G();let y=M=>{let se=M.detail;Array.isArray(se)&&n(se)},ae=()=>{Q("/disks/new")},E=M=>{M.detail==="disks"&&G()};return window.addEventListener("rookery:disks-data",y),window.addEventListener("rookery:add-disk",ae),window.addEventListener("rookery:tab-changed",E),()=>{window.removeEventListener("rookery:disks-data",y),window.removeEventListener("rookery:add-disk",ae),window.removeEventListener("rookery:tab-changed",E)}},[]),z(()=>{let y=document.getElementById("disk-tab-count");y&&(y.textContent=String(t.length))},[t.length]);let J=async y=>{if(y.attached_vm_id){oe(`Detach ${y.name} from ${y.attached_vm_name||"its VM"} before deleting it.`,!0);return}if(window.confirm(y.source==="host-device"?`Delete disk "${y.name}"? +var Pe,aa,Ro,st,ea,Et,sa,Tt,oa,nn,tn,Fo,Lt={},It=[],Bo=/^m(i|n|o|s|text|space)$/,an=Array.isArray,sn=It.slice,lt=Object.assign;function At(t){t&&t.parentNode&&t.remove()}function ia(t,n,a){var l,s,o,f={},r=arguments.length;for(o in n)o=="key"?l=n[o]:o=="ref"&&typeof t!="function"?s=n[o]:f[o]=n[o];return r>2&&(f.children=r>3?sn.call(arguments,2):a),ht(t,f,l,s,null)}function ht(t,n,a,l,s){var o={type:t,props:n,key:a,ref:l,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:s||++aa,__i:-1,__u:0};return!s&&Pe.vnode&&Pe.vnode(o),o}function ge(t){return t.children}function Pt(t,n){this.props=t,this.context=n,this.__g=0}function ot(t,n){if(n==null)return t.__?ot(t.__,t.__i+1):null;for(var a;nn&&st.sort(sa),t=st.shift(),n=st.length,8&t.__g&&(l=void 0,s=void 0,o=(s=(a=t).__v).__e,f=[],r=[],(b=a.__P)&&((l=lt({constructor:void 0},s)).__v=s.__v+1,Pe.vnode&&Pe.vnode(l),on(b,l,s,a.__n,b.namespaceURI,32&s.__u?[o]:null,f,o||ot(s),32&s.__u,r),l.__v=s.__v,l.__.__k[l.__i]=l,ua(f,l,r),s.__=s.__e=null,l.__e!=o&&ra(l)))}finally{st.length=Et=0}}function la(t,n,a,l,s,o,f,r,b,w,d){var C,h,A,I,m,y,P=l.__k||It,L=n.length;for(b=Oo(a,n,P,b,L),C=0;Cb?P--:P++,L=!0)):(~w||(s>m?P--:s>1](d?1:0)){for(s=a-1,o=a+1;s>=0||o=0?s--:o++])&&!(2&w.__u)&&r==w.key&&b==w.type)return f}return-1}function na(t,n,a){a==null&&(a=""),n[0]=="-"?t.setProperty(n,a):t[n]=a}function St(t,n,a,l,s){var o,f;e:if(n=="style")if(typeof a=="string")t.style.cssText=a;else{if(typeof l=="string"&&(t.style.cssText=l=""),l)for(n in l)a&&n in a||na(t.style,n,"");if(a)for(n in a)l&&a[n]==l[n]||na(t.style,n,a[n])}else if(n[0]=="o"&&n[1]=="n")(t.__e||(t.__e={}))[n]=a,a&&l||(f=tn[n]||(tn[n]=qo(n)),(t.__a||(t.__a={}))[n]=nn,o=n!=(n=n.replace(oa,"$1")),(n=n.slice(2))[0]<"a"&&(n=n.toLowerCase()),a?t.addEventListener(n,f,o):t.removeEventListener(n,f,o));else{if(s=="http://www.w3.org/2000/svg")n=n.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(n!="width"&&n!="height"&&n!="href"&&n!="list"&&n!="form"&&n!="tabIndex"&&n!="download"&&n!="rowSpan"&&n!="colSpan"&&n!="role"&&n!="popover"&&n in t)try{t[n]=a??"";break e}catch{}typeof a=="function"||(a==null||a===!1&&n[4]!="-"?t.removeAttribute(n):t.setAttribute(n,n=="popover"&&a==1?"":a))}}function qo(t){return function(n){if(this.__e){var a=this.__e[t];if(n[Tt]==null)n[Tt]=nn++;else if(n[Tt]=a.__.length&&a.__.push({}),a.__[t]}function c(t){return Nt=1,Go(Ca,t)}function Go(t,n,a){var l=fn(gt++,2);if(l.t=t,!l.__c&&(l.__=[a?a(n):Ca(void 0,n),function(o){var f=l.__N?l.__N[0]:l.__[0],r=l.t(f,o);dn(f,r)||(l.__N=[r,l.__[1]],l.__c.setState({}))}],l.__c=Re,!Re.__f)){Re.__f=!0;var s=Re.shouldComponentUpdate;Re.shouldComponentUpdate=function(o,f,r){var b=this.__H;if(!b)return!0;var w=!1,d=this.props!=o;if(b.__.some(function(h){h.__N&&(w=!0,dn(h.__[0],h.__N[0])||(d=!0))}),s){var C=s.call(this,o,f,r);return w?C||d:C}return!w||d}}return l.__}function G(t,n){var a=fn(gt++,3);!Me.__s&&ka(a.__H,n)&&(a.__P=!0,a.__=t,a.u=n,Re.__H.__h.push(a))}function ne(t){return Nt=5,We(function(){return{current:t}},[])}function We(t,n){var a=fn(gt++,7);return ka(a.__H,n)&&(a.__=t(),a.__H=n),a.__}function Ko(){var t;do{for(;t=un.shift();)try{bt(t)}catch(a){Me.__e(a,{__:(t=t.__P)&&t.__v})}for(;t=wa.shift();){var n=t.__H;if(t.__P&&n)try{n.__h.some(bt),n.__h.some(pn),n.__h=[]}catch(a){n.__h=[],Me.__e(a,t.__v)}}}while(un.length)}Me.__b=function(t){Re=null,fa&&fa(t)},Me.__=function(t,n){t&&n.__k&&n.__k.__m&&(t.__m=n.__k.__m),va&&va(t,n)},Me.__r=function(t){ma&&ma(t),gt=0;var n=(Re=t.__c).__H;n&&(cn==Re?Re.__h=[]:(n.__h.some(bt),n.__h.some(pn),gt=0),n.__h=[],n.__.some(function(a){a.__N&&(a.__=a.__N),a.u=a.__N=void 0})),cn=Re},Me.diffed=function(t){ha&&ha(t);var n=t.__c;n&&n.__H&&(n.__H.__h.length&&_a(wa.push(n)),n.__H.__.some(function(a){a.u&&(a.__H=a.u)})),cn=Re=null},Me.__c=function(t,n){n.some(function(a){try{a.__h.some(bt),a.__h=a.__h.filter(function(l){return!l.__||pn(l)})}catch(l){n.some(function(s){s.__h&&(s.__h=[])}),n=[],Me.__e(l,a.__v)}}),ba&&ba(t,n)},Me.unmount=function(t){ga&&ga(t);var n,a,l=t.__c;l&&l.__H&&(l.__H.__.some(function(s){try{if(s.__P&&s.__c){if(a===void 0){for(a=t.__;a&&(!a.__c||!a.__c.__P);)a=a.__;a=a&&a.__c}s.__P=a,_a(un.push(s))}else bt(s)}catch(o){n=o}}),l.__H=void 0,n&&Me.__e(n,l.__v))};var ya=typeof requestAnimationFrame=="function";function jo(t){var n,a=function(){clearTimeout(l),ya&&cancelAnimationFrame(n),setTimeout(t)},l=setTimeout(a,35);ya&&(n=requestAnimationFrame(a))}function _a(t){t!=1&&pa==Me.requestAnimationFrame||((pa=Me.requestAnimationFrame)||jo)(Ko)}function bt(t){var n=Re,a=t.__c;typeof a=="function"&&(t.__c=void 0,a()),Re=n}function pn(t){var n=Re;t.__c=t.__(),Re=n}function ka(t,n){return!t||t.length!=n.length||n.some(function(a,l){return!dn(a,t[l])})}function Ca(t,n){return typeof n=="function"?n(t):n}var Jo=0;function e(t,n,a,l,s,o){n||(n={});var f,r,b=n;if("ref"in b&&typeof t!="function")for(r in b={},n)r=="ref"?f=n[r]:b[r]=n[r];var w={type:t,props:b,key:a,ref:f,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--Jo,__i:-1,__u:0};return(s||o)&&(w.__source=s,w.__self=o),Pe.vnode&&Pe.vnode(w),w}var xt=null,Dt=[],Xo=1;function ae(t,n=!1){let a={id:Xo++,message:t,error:n};xt?xt(a):Dt.push(a)}function Zo(){let[t,n]=c(null),a=ne(null);return G(()=>{let l=s=>{a.current!==null&&window.clearTimeout(a.current),n(s),a.current=window.setTimeout(()=>{n(null),a.current=null},3500)};if(xt=l,Dt.length>0){let s=Dt;Dt=[];for(let o of s)l(o)}return()=>{xt=null,a.current!==null&&(window.clearTimeout(a.current),a.current=null)}},[]),t?e("div",{class:t.error?"toast error":"toast",role:"status","aria-live":"polite",children:t.message}):null}function Sa(t){Fe(e(Zo,{}),t)}var Xe=class extends Error{constructor(n,a){super(n),this.name="APIError",this.status=a}},Ia=[1e3,2e3,4e3,1e4],Mt=Ia.length+1,Yo=new Set([429,503]),Qo=new Set([408,425,429,500,502,503,504]),Ta=new Set(["GET","HEAD","PUT","DELETE","OPTIONS"]),Vt=new Map;function Aa(t){return(t.method||"GET").toUpperCase()}function Pa(){return typeof DOMException=="function"?new DOMException("The request was aborted","AbortError"):Object.assign(new Error("The request was aborted"),{name:"AbortError"})}function ei(t,n){return new Promise((a,l)=>{if(n?.aborted){l(Pa());return}let s=window.setTimeout(()=>{n?.removeEventListener("abort",o),a()},t),o=()=>{window.clearTimeout(s),l(Pa())};n?.addEventListener("abort",o,{once:!0})})}function ti(t,n,a){return a?.aborted?!1:n.status===void 0?Ta.has(t):Yo.has(n.status)?!0:Ta.has(t)&&Qo.has(n.status)}function Ea(t,n,a){let l=a.status===void 0?a.message||"network error":`${a.message||"request failed"} (HTTP ${a.status})`;return new Xe(`${t} ${n} failed after ${Mt} attempts: ${l}`,a.status)}async function ni(t,n){let a=new Headers(n.headers||{});n.body!==void 0&&a.set("Content-Type","application/json");let l;try{l=await fetch(t,{...n,headers:a})}catch(o){throw o instanceof Error&&o.name==="AbortError"?o:new Xe(o instanceof Error?o.message:String(o))}let s=null;if(l.status!==204){let o=await l.text();if(o)try{s=JSON.parse(o)}catch{s=null}}if(!l.ok){let o=`Request failed (${l.status})`;throw s&&typeof s=="object"&&("error"in s&&typeof s.error=="string"?o=s.error:"message"in s&&typeof s.message=="string"&&(o=s.message)),new Xe(o,l.status)}return s}async function La(t,n){let a=Aa(n),l;for(let s=1;s<=Mt;s+=1)try{return await ni(t,n)}catch(o){if(!(o instanceof Xe)||(l=o,!ti(a,o,n.signal)))throw o;if(s>=Mt)throw Ea(a,t,o);let f=Ia[s-1];console.warn(`${a} ${t} failed (attempt ${s}/${Mt}); retrying in ${f/1e3}s: ${o.message}`),await ei(f,n.signal)}throw Ea(a,t,l??new Xe("request failed"))}function V(t,n={}){if(Aa(n)!=="GET"||n.signal)return La(t,n);let l=`GET ${t}`,s=Vt.get(l);if(s)return s;let o=La(t,n).finally(()=>{Vt.get(l)===o&&Vt.delete(l)});return Vt.set(l,o),o}function ai(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function mn(t,n){let a=new Blob([n],{type:"application/x-pem-file"}),l=URL.createObjectURL(a),s=document.createElement("a");s.href=l,s.download=t,s.click(),URL.revokeObjectURL(l)}function Na(){let t=ne(null),[n,a]=c(!1),[l,s]=c(!0),[o,f]=c([]),[r,b]=c(""),[w,d]=c("365"),[C,h]=c(null),[A,I]=c(""),[m,y]=c(!1),[P,L]=c(!1);G(()=>{if(!n){t.current?.close();return}t.current?.showModal(),h(null),I(""),S()},[n]);let S=async()=>{y(!0);try{let T=await fetch("/api/pki/clients",{headers:{Accept:"application/json"}}),B=await T.text(),K=null;if(B)try{K=JSON.parse(B)}catch{K=null}if(!T.ok){let Z=K&&typeof K=="object"&&"error"in K&&typeof K.error=="string"?K.error:`Request failed (${T.status})`;throw new Xe(Z,T.status)}f(Array.isArray(K)?K:[]),s(!0),I("")}catch(T){T instanceof Xe&&T.status===503?(s(!1),I("mTLS is not enabled on this server. Start rookery with -mtls-addr and -pki-dir.")):I(T instanceof Error?T.message:String(T))}finally{y(!1)}},N=async T=>{if(T.preventDefault(),!(P||!r.trim())){L(!0),I("");try{let B=await V("/api/pki/clients",{method:"POST",body:JSON.stringify({name:r.trim(),days:Number(w)||365})});h(B),b(""),await S(),ae(`Issued client certificate for ${B.name}`)}catch(B){I(B instanceof Error?B.message:String(B))}finally{L(!1)}}},p=async T=>{if(window.confirm(`Revoke the certificate for ${T.name}? Existing connections will fail on the next handshake.`)){L(!0),I("");try{await V(`/api/pki/clients/${T.id}`,{method:"DELETE"}),await S(),ae(`Revoked client certificate for ${T.name}`)}catch(B){I(B instanceof Error?B.message:String(B))}finally{L(!1)}}};return e("div",{class:"sidebar-settings",children:[e("p",{class:"eyebrow",children:"Access"}),e("button",{class:"tab",type:"button",onClick:()=>a(!0),children:"Client certificates"}),e("span",{class:"field-hint",children:"Public mTLS API access"}),e("dialog",{ref:t,class:"dialog dialog-wide","aria-labelledby":"client-certs-title",onClose:()=>a(!1),onClick:T=>{T.target===T.currentTarget&&a(!1)},onCancel:T=>{P&&T.preventDefault()},children:e("div",{class:"dialog-body",children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"mTLS access"}),e("h2",{id:"client-certs-title",children:"Client certificates"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>a(!1),children:"\xD7"})]}),l?e(ge,{children:[e("p",{class:"field-hint",children:"Clients connect to the public mTLS listener and must present a certificate signed by this server's CA. Private keys are shown once and never stored by Rookery."}),e("div",{class:"actions",children:e("a",{class:"button ghost",href:"/api/pki/ca",download:"rookery-ca.pem",children:"Download CA certificate"})}),e("form",{class:"form-grid",onSubmit:T=>{N(T)},children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Client name"}),e("input",{type:"text",required:!0,maxlength:64,autocomplete:"off",spellcheck:!1,placeholder:"ops-laptop",value:r,onInput:T=>b(T.currentTarget.value)}),e("span",{class:"field-hint",children:"Used as the certificate common name."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Validity (days)"}),e("input",{type:"number",required:!0,min:1,max:3650,value:w,onInput:T=>d(T.currentTarget.value)})]}),e("div",{class:"field field-wide actions",children:e("button",{class:"button primary",type:"submit",disabled:P||!r.trim(),children:P?"Issuing\u2026":"Issue certificate"})})]})]}):e("div",{class:"banner",role:"alert",children:A||"mTLS is not enabled on this server."}),C?e("section",{class:"panel panel-spaced","aria-labelledby":"issued-cert-title",children:[e("div",{class:"panel-head",children:e("div",{children:[e("h3",{id:"issued-cert-title",children:["Certificate for ",C.name]}),e("p",{class:"panel-subtitle",children:"Download or copy these files now; the private key will not be shown again."})]})}),e("div",{class:"actions",children:[e("button",{class:"button primary",type:"button",onClick:()=>mn("client.crt",C.certificate_pem),children:"Download client.crt"}),e("button",{class:"button primary",type:"button",onClick:()=>mn("client.key",C.private_key_pem),children:"Download client.key"}),e("button",{class:"button ghost",type:"button",onClick:()=>mn("ca.crt",C.ca_pem),children:"Download ca.crt"})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Client certificate (PEM)"}),e("textarea",{class:"cert-pem",readOnly:!0,rows:5,value:C.certificate_pem})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Client private key (PEM)"}),e("textarea",{class:"cert-pem",readOnly:!0,rows:5,value:C.private_key_pem})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"CA certificate (PEM)"}),e("textarea",{class:"cert-pem",readOnly:!0,rows:4,value:C.ca_pem})]})]}):null,A&&l?e("p",{class:"form-error",role:"alert",children:A}):null,l?e("div",{class:"table-wrap",children:m?e("p",{class:"field-hint",children:"Loading issued certificates\u2026"}):o.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Serial"}),e("th",{scope:"col",children:"Expires"}),e("th",{scope:"col",children:"Status"}),e("th",{scope:"col"})]})}),e("tbody",{children:o.map(T=>e("tr",{children:[e("td",{children:T.name}),e("td",{class:"mono",children:[T.serial.slice(0,16),"\u2026"]}),e("td",{children:ai(T.not_after)}),e("td",{children:e("span",{class:"status-badge","data-status":T.revoked?"disabled":"running",children:T.revoked?"revoked":"valid"})}),e("td",{children:T.revoked?null:e("button",{class:"row-action danger",type:"button",disabled:P,onClick:()=>{p(T)},children:"Revoke"})})]},T.id))})]}):e("p",{class:"field-hint",children:"No client certificates have been issued yet."})}):null,e("footer",{class:"dialog-actions",children:e("button",{class:"button ghost",type:"button",onClick:()=>a(!1),children:"Close"})})]})})]})}var vt="rookery:route-changed";function $t(){let t=window.location.pathname.replace(/\/+$/,"")||"/";return{path:t,segments:t.split("/").filter(Boolean),query:new URLSearchParams(window.location.search),search:window.location.search}}function te(t,n=!1){let a=t.startsWith("/")?t:`/${t}`;`${window.location.pathname}${window.location.search}`!==a&&(n?window.history.replaceState(null,"",a):window.history.pushState(null,"",a),window.dispatchEvent(new CustomEvent(vt)))}function ze(){let[t,n]=c($t);return G(()=>{let a=()=>n($t());return window.addEventListener("popstate",a),window.addEventListener(vt,a),()=>{window.removeEventListener("popstate",a),window.removeEventListener(vt,a)}},[]),t}var Ze=["vms","disks","media","networks","vnfs","lightvm","settings"],si={vms:{eyebrow:"Thin-LVM inventory",title:"Virtual machines",lede:"Create instances, attach disks and installation media, start them with QEMU/KVM, and open their consoles."},disks:{eyebrow:"Thin-LVM inventory",title:"Disks",lede:"Create and manage instance disks separately, then attach an unattached disk to a VM."},media:{eyebrow:"Installation media",title:"Installation media",lede:"Register read-only ISO files from the host, then choose one as a VM's boot media."},networks:{eyebrow:"VPP bridge domains",title:"Network",lede:"Create VPP bridge domains and vhost-user interfaces, then attach them to stopped VMs."},vnfs:{eyebrow:"Network functions",title:"VNFs",lede:"Attach DHCPv4 and DHCPv6 functions to Rookery Networks, enable them, and inspect their leases."},lightvm:{eyebrow:"LightVM inventory",title:"LightVM",lede:"Upload kernels and initramfs images, then define container-backed microVMs."},settings:{eyebrow:"Rookery server",title:"Settings",lede:"Configure cgroup resource limits, server-wide Rookery behavior, and lifecycle hooks."}};function ye(t){let n=document.getElementById(t);if(!n)throw new Error(`missing UI element #${t}`);return n}function Da(){let t=ye("api-status"),n=ye("api-status-label"),a=ye("hero-eyebrow"),l=ye("hero-title"),s=ye("hero-lede"),o=ye("readiness-summary"),f=ye("thin-pool-card"),r=ye("thin-pool-state"),b=ye("thin-pool-target"),w=ye("thin-pool-detail"),d=ye("vpp-card"),C=ye("vpp-state"),h=ye("vpp-target"),A=ye("vpp-detail"),I={card:ye("rookery-startup-script-card"),state:ye("rookery-startup-script-state"),target:ye("rookery-startup-script-target"),detail:ye("rookery-startup-script-detail")},m={card:ye("vpp-startup-script-card"),state:ye("vpp-startup-script-state"),target:ye("vpp-startup-script-target"),detail:ye("vpp-startup-script-detail")},y={vms:ye("tab-vms"),disks:ye("tab-disks"),media:ye("tab-media"),networks:ye("tab-networks"),vnfs:ye("tab-vnfs"),lightvm:ye("tab-lightvm"),settings:ye("tab-settings")},P={vms:ye("vms-panel"),disks:ye("disks-panel"),media:ye("media-panel"),networks:ye("networks-panel"),vnfs:ye("vnfs-panel"),lightvm:ye("lightvm-panel"),settings:ye("settings-panel")},L={vms:ye("new-vm"),disks:ye("new-disk"),media:ye("new-media"),networks:ye("new-network")},S="vms",N={ready:"ready",checking:"checking",missing:"missing",not_configured:"not configured",unreachable:"unreachable",error:"error"},p=E=>{let z={online:"api online",offline:"api offline",pending:"connecting"};t.dataset.state=E,n.textContent=z[E]},T=(E,z,re,ue,v,M)=>{let k=v?.state||"checking";E.dataset.state=k,z.textContent=N[k]||k,re.textContent=M,re.title=M;let O=v?.detail||"";!O&&k==="checking"&&(O="Checking\u2026"),ue.textContent=O},B=E=>{if(!E)return"";let z=new Date(E);return Number.isNaN(z.getTime())?"":z.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})},K=(E,z,re)=>{let ue="unreachable",v="unavailable",M="status unavailable",k="Could not read the startup script status.";if(z){let O=z.status||(z.enabled?"pending":"disabled"),le=B(z.ran_at);switch(O){case"success":ue="ready",v="success",M=le?`last run ${le}`:"last run succeeded",k=`exit code ${z.exit_code??0}`;break;case"failed":ue="error",v="failed",M=le?`last run ${le}`:"last run failed",k=`exit code ${z.exit_code??"\u2014"} \xB7 open Settings for output`;break;case"running":ue="checking",v="running",M="running now",k=le?`started ${le}`:"Waiting for completion\u2026";break;case"pending":ue="missing",v="pending",M="not run yet",k=re==="vpp"?"Runs on the next VPP connection":"Runs on the next API start";break;case"disabled":ue="not_configured",v="disabled",M="disabled",k="Enable it in Settings";break;case"unavailable":ue="unreachable",v="unavailable",M="status unavailable",k="Could not read the startup script status.";break}}E.card.dataset.state=ue,E.state.textContent=v,E.target.textContent=M,E.target.title=M,E.detail.textContent=k},Z=E=>{let z=!!E.ready,re=E.thin_pool,ue=E.vpp,v="not configured";if(re?.vg&&re.name?v=`${re.vg}/${re.name}`:re?.vg&&(v=re.vg),T(f,r,b,w,re,v),T(d,C,h,A,ue,ue?.socket||"not configured"),K(I,E.rookery_startup_script,"rookery"),K(m,E.vpp_startup_script,"vpp"),z)o.dataset.state="ready",o.textContent="ready for VMs";else{let O=[re?.state,ue?.state].filter(Boolean);O.includes("unreachable")||O.includes("error")?o.dataset.state="unreachable":O.includes("missing")||O.includes("not_configured")?o.dataset.state="missing":o.dataset.state="checking",o.textContent=o.dataset.state==="checking"?"checking":"VM creation blocked"}let M=!!re?.ready,k=!!ue?.ready;window.dispatchEvent(new CustomEvent("rookery:readiness-changed",{detail:{ready:z,thinPool:M}})),window.dispatchEvent(new CustomEvent("rookery:vpp-ready",{detail:k}))},ie=()=>{T(f,r,b,w,{state:"unreachable",detail:"Cannot reach the API."},"status unavailable"),T(d,C,h,A,{state:"unreachable",detail:"Cannot reach the API."},"status unavailable"),K(I,void 0,"rookery"),K(m,void 0,"vpp"),o.dataset.state="unreachable",o.textContent="status unavailable",window.dispatchEvent(new CustomEvent("rookery:readiness-changed",{detail:{ready:!1,thinPool:!1}})),window.dispatchEvent(new CustomEvent("rookery:vpp-ready",{detail:!1}))},x=async()=>{try{let E=await V("/api/status");Z(E),p("online")}catch{p("offline"),ie()}},R=E=>Ze.includes(E),F=E=>{let z=E.split("/").filter(Boolean)[0]||"vms";return z==="storage-backends"?"disks":z==="interfaces"||z==="acls"||z==="route-tables"?"networks":R(z)?z:"vms"},W=(E,z=!1,re=!0)=>{let ue=R(E)?E:"vms",v=ue!==S;S=ue;for(let k of Ze){let O=k===ue;y[k].setAttribute("aria-selected",String(O)),y[k].tabIndex=O?0:-1,P[k].hidden=!O;let le=L[k];le&&(le.hidden=!O)}re&&te(`/${ue}`);let M=si[ue];a.textContent=M.eyebrow,l.textContent=M.title,s.textContent=M.lede,document.title=`${M.title} \xB7 rookery`,v&&window.dispatchEvent(new CustomEvent("rookery:tab-changed",{detail:ue})),z&&y[ue].focus()},Q=E=>{if(!["ArrowLeft","ArrowRight","Home","End"].includes(E.key))return;E.preventDefault();let re=Ze.indexOf(S);E.key==="Home"?W(Ze[0],!0):E.key==="End"?W(Ze[Ze.length-1],!0):E.key==="ArrowLeft"?W(Ze[(re-1+Ze.length)%Ze.length],!0):W(Ze[(re+1)%Ze.length],!0)},g=()=>{let E=$t();E.path==="/"&&window.history.replaceState(null,"","/vms");let z=E.path.startsWith("/route-tables")||E.path==="/vnfs/new"||/^\/vnfs\/[^/]+\/edit$/.test(E.path);document.body.classList.toggle("route-page-mode",z),W(F(E.path),!1,!1)};for(let E of Ze)y[E].addEventListener("click",()=>W(E)),y[E].addEventListener("keydown",Q);L.vms?.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("rookery:add-vm"))}),L.disks?.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("rookery:add-disk"))}),L.media?.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("rookery:add-media"))}),L.networks?.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("rookery:add-network"))}),window.addEventListener("popstate",g),window.addEventListener(vt,g),window.addEventListener("rookery:open-tab",E=>{let z=String(E.detail||"");R(z)&&W(z)}),window.setInterval(()=>{document.hidden||x()},1e4);let oe=window.location.hash.replace(/^#/,"");oe&&R(oe)&&window.history.replaceState(null,"",`/${oe}`),g(),x()}var Rt=64;function Ne(t){let[n,a]=c({}),l=async()=>{try{let o=await V(`/api/labels/${encodeURIComponent(t)}`),f={};for(let r of Array.isArray(o)?o:[])f[r.entity_id]=Array.isArray(r.labels)?r.labels:[];a(f)}catch{a({})}};return G(()=>{l()},[t]),G(()=>{let o=f=>{let r=f.detail;!r||r.entityType!==t||!r.entityID||a(b=>({...b,[r.entityID]:Array.isArray(r.labels)?r.labels:[]}))};return window.addEventListener("rookery:labels-changed",o),()=>window.removeEventListener("rookery:labels-changed",o)},[t]),{labels:n,reload:l,setEntityLabels:(o,f)=>{a(r=>({...r,[o]:f}))}}}function ke(t,n){let[a,l]=c([]);return G(()=>{if(n==null||String(n)===""){l([]);return}let s=!0;return V(`/api/labels/${encodeURIComponent(t)}/${encodeURIComponent(String(n))}`).then(o=>{s&&l(Array.isArray(o.labels)?o.labels:[])}).catch(()=>{s&&l([])}),()=>{s=!1}},[t,n]),[a,l]}async function xa(t,n,a){let l=await V(`/api/labels/${encodeURIComponent(t)}/${encodeURIComponent(String(n))}`,{method:"PUT",body:JSON.stringify({labels:a})}),s=Array.isArray(l.labels)?l.labels:[];return window.dispatchEvent(new CustomEvent("rookery:labels-changed",{detail:{entityType:t,entityID:String(n),labels:s}})),s}async function Ce(t,n,a,l){try{await xa(t,n,l)}catch(s){ae(`Saved ${a}, but labels could not be updated: ${s instanceof Error?s.message:String(s)}`,!0)}}function Se({labels:t,onChange:n,disabled:a=!1}){let[l,s]=c(""),o=Array.isArray(t)?t:[],f=()=>{let r=l.split(/[\n,]+/).map(w=>w.trim()).filter(Boolean);if(r.length===0)return;let b=[...o];for(let w of r)b.includes(w)||b.push(w);n(b),s("")};return e("div",{class:"labels-field",children:[e("span",{class:"field-label",children:"Labels"}),e("div",{class:"labels-editor-list",children:o.length?o.map(r=>e("span",{class:"label-chip editable",children:[r,e("button",{type:"button","aria-label":`Remove ${r}`,disabled:a,onClick:()=>n(o.filter(b=>b!==r)),children:"\xD7"})]},r)):e("span",{class:"field-hint",children:"No labels yet."})}),e("div",{class:"labels-editor-input",children:[e("input",{type:"text",value:l,maxLength:256,disabled:a||o.length>=Rt,placeholder:"production, web, critical",onInput:r=>s(r.currentTarget.value),onKeyDown:r=>{r.key==="Enter"&&(r.preventDefault(),f())}}),e("button",{class:"button ghost",type:"button",disabled:a||!l.trim()||o.length>=Rt,onClick:f,children:"Add"})]}),e("span",{class:"field-hint",children:["Add as many labels as you need. Commas separate multiple labels; up to ",Rt,"."]})]})}function De({entityType:t,entityID:n,name:a,labels:l,onSaved:s}){let[o,f]=c(!1),r=Array.isArray(l)?l:[];return e("div",{class:"entity-labels",children:[r.map(b=>e("span",{class:"label-chip",title:b,children:b},b)),e("button",{class:"label-add",type:"button","aria-label":`Edit labels for ${a}`,onClick:()=>f(!0),children:r.length?"edit labels":"+ label"}),o?e(oi,{entityType:t,entityID:String(n),name:a,labels:r,onClose:()=>f(!1),onSaved:b=>{s(b),f(!1)}}):null]})}function oi({entityType:t,entityID:n,name:a,labels:l,onClose:s,onSaved:o}){let f=ne(null),r=ne(null),[b,w]=c(l),[d,C]=c(""),[h,A]=c(""),[I,m]=c(!1);G(()=>{f.current?.showModal(),r.current?.focus()},[]);let y=()=>{I||f.current?.close()},P=()=>{let S=d.split(/[\n,]+/).map(N=>N.trim()).filter(Boolean);S.length!==0&&(w(N=>{let p=[...N];for(let T of S)p.includes(T)||p.push(T);return p}),C(""),A(""))},L=async S=>{if(S.preventDefault(),!I){m(!0),A("");try{let N=await xa(t,n,b);o(N),ae(`Saved labels for ${a}`)}catch(N){A(N instanceof Error?N.message:String(N))}finally{m(!1)}}};return e("dialog",{ref:f,class:"dialog labels-dialog","aria-labelledby":"labels-dialog-title",onClose:s,onClick:S=>{S.target===S.currentTarget&&y()},onCancel:S=>{I&&S.preventDefault()},children:e("form",{onSubmit:S=>{L(S)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Labels"}),e("h2",{id:"labels-dialog-title",children:a})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:y,children:"\xD7"})]}),e("div",{class:"labels-editor",children:[e("span",{class:"field-hint",children:["Add as many labels as you need. Labels are free-form text; commas separate multiple labels. Up to ",Rt," per ",t,"."]}),e("div",{class:"labels-editor-list",children:b.length?b.map(S=>e("span",{class:"label-chip editable",children:[S,e("button",{type:"button","aria-label":`Remove ${S}`,disabled:I,onClick:()=>w(N=>N.filter(p=>p!==S)),children:"\xD7"})]},S)):e("span",{class:"field-hint",children:"No labels yet."})}),e("div",{class:"labels-editor-input",children:[e("input",{ref:r,type:"text",value:d,maxLength:256,placeholder:"production, web, critical",onInput:S=>C(S.currentTarget.value),onKeyDown:S=>{S.key==="Enter"&&(S.preventDefault(),P())}}),e("button",{class:"button ghost",type:"button",disabled:!d.trim()||I,onClick:P,children:"Add"})]}),h?e("p",{class:"form-error",role:"alert",children:h}):null]}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:I,onClick:y,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:I,children:I?"Saving\u2026":"Save labels"})]})]})})}var ii="[A-Za-z0-9][A-Za-z0-9._\\-]*";function $a({backends:t,disks:n,onClose:a,onSaved:l}){let s=t.find(_=>_.name==="default")||t[0],[o,f]=c(t.length===0?"host":"lvm"),[r,b]=c(""),[w,d]=c("10"),[C,h]=c(s?String(s.id):""),[A,I]=c([]),[m,y]=c(""),[P,L]=c(!1),[S,N]=c(""),[p,T]=c("empty"),[B,K]=c(""),[Z,ie]=c(!1),[x,R]=c(""),[F,W]=c("5m"),[Q,g]=c("{}"),[oe,E]=ke("disk"),[z,re]=c(!1),[ue,v]=c(""),M=ne(null),k=ne(null),O=ne(null);G(()=>{M.current?.showModal(),k.current?.focus()},[]);let le=t.find(_=>String(_.id)===C)||null,se=!!le?.config?.supports_templates,fe=n.filter(_=>_.is_template&&String(_.storage_backend_id??"")===C),ee=fe.find(_=>String(_.id)===B)||null,de=A.find(_=>_.path===m)||null;G(()=>{se||(T("empty"),K(""),ie(!1))},[se]),G(()=>{if(o!=="host")return;let _=!1;return L(!0),N(""),V("/api/host/block-devices/attachable").then($=>{_||I(Array.isArray($)?$:[])}).catch($=>{_||(I([]),N($ instanceof Error?$.message:String($)))}).finally(()=>{_||L(!1)}),()=>{_=!0}},[o]);let me=()=>{z||M.current?.close()},u=async _=>{if(_.preventDefault(),z||!O.current?.reportValidity())return;if(o==="host"&&(!de||!de.available)){v("Select an available host block device.");return}let $;if(o==="lvm"&&p==="template"&&ee?.provisioning_script){try{$=JSON.parse(Q||"{}")}catch{v("Provisioning properties must be valid JSON.");return}if($===null||Array.isArray($)||typeof $!="object"){v("Provisioning properties must be a JSON object.");return}}v(""),re(!0);try{let j=o==="host"?{name:r.trim(),host_device:de?.path}:{name:r.trim(),size_gb:Number(w),storage_backend_id:Number(C),...p==="template"&&ee?{template_disk_id:ee.id}:{},...p==="template"&&ee?.provisioning_script&&$!==void 0?{provisioning:$}:{},...p==="empty"&&se?{is_template:Z,...Z?{provisioning_script:x.trim(),provisioning_timeout:F.trim()}:{}}:{}},J=await V("/api/disks",{method:"POST",body:JSON.stringify(j)});await Ce("disk",J.id,J.name,oe),l(J)}catch(j){v(j instanceof Error?j.message:String(j))}finally{re(!1)}};return e("dialog",{ref:M,id:"disk-dialog","aria-labelledby":"disk-dialog-title",onClose:a,onClick:_=>{_.target===_.currentTarget&&me()},onCancel:_=>{z&&_.preventDefault()},children:e("form",{ref:O,novalidate:!0,onSubmit:_=>{u(_)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"New disk"}),e("h2",{id:"disk-dialog-title",children:"Create disk"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:me,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:k,type:"text",required:!0,maxlength:127,pattern:ii,autocomplete:"off",spellcheck:!1,placeholder:"vm01-root",value:r,onInput:_=>b(_.currentTarget.value)}),e("span",{class:"field-hint",children:"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Storage source"}),e("select",{value:o,onChange:_=>f(_.currentTarget.value),children:[e("option",{value:"lvm",disabled:t.length===0,children:"Provisioned storage (LVM thin or kouja)"}),e("option",{value:"host",children:"Host block device (passthrough)"})]}),e("span",{class:"field-hint",children:"Provisioned disks use the selected storage backend. Passthrough disks hand an existing host disk, partition, or logical volume to one VM; Rookery never deletes or resizes the underlying device."})]}),o==="lvm"?e(ge,{children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Size (GB)"}),e("input",{type:"number",required:!0,min:ee?ee.size_gb:1,max:65536,step:1,value:w,onInput:_=>d(_.currentTarget.value)}),e("span",{class:"field-hint",children:"Creates an LVM thin volume of this size in the configured pool."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Storage backend"}),e("select",{required:!0,value:C,onChange:_=>{h(_.currentTarget.value),K(""),T("empty")},children:t.map(_=>e("option",{value:String(_.id),children:[_.name," \xB7 ",_.driver==="lvm-thin"?"LVM thin":_.driver==="kouja"?"Kouja":_.driver]},_.id))}),e("span",{class:"field-hint",children:le?.driver==="kouja"?"Kouja provisions a remote volume and exports it over the selected protocol.":se?"Template cloning is available on this backend.":"Template cloning is disabled for this backend; enable it in Storage backends."})]}),se?e("label",{class:"field",children:[e("span",{class:"field-label",children:"Creation source"}),e("select",{value:p,onChange:_=>T(_.currentTarget.value),children:[e("option",{value:"empty",children:"Empty disk"}),e("option",{value:"template",disabled:fe.length===0,children:["From template",fe.length===0?" (none available)":""]})]}),e("span",{class:"field-hint",children:"Template clones run the backend preparation script and the template's provisioning script when one is configured; empty disks use the thin pool directly."})]}):null,se&&p==="template"?e("label",{class:"field",children:[e("span",{class:"field-label",children:"Template"}),e("select",{required:!0,value:B,onChange:_=>{let $=_.currentTarget.value;K($);let j=fe.find(J=>String(J.id)===$);j&&d(String(j.size_gb))},children:[e("option",{value:"",children:"Select a template\u2026"}),fe.map(_=>e("option",{value:String(_.id),children:[_.name," \xB7 ",_.size_gb," GB",_.provisioning_script?" \xB7 provisioning":""]},_.id))]})]}):null,se&&p==="empty"?e("label",{class:"field field-wide checkbox-field",children:e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:Z,onChange:_=>ie(_.currentTarget.checked)}),e("span",{children:"Mark this new disk as a template"})]})}):null,se&&p==="empty"&&Z?e(ge,{children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"VM provisioning script"}),e("input",{type:"text",placeholder:"/usr/local/sbin/rookery-provision-template",value:x,onInput:_=>R(_.currentTarget.value)}),e("span",{class:"field-hint",children:["Optional absolute-path script run after a clone is created. It receives",e("code",{children:"ROOKERY_PROVISION_JSON"}),"."]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Provisioning timeout"}),e("input",{type:"text",placeholder:"5m",value:F,onInput:_=>W(_.currentTarget.value)})]})]}):null,se&&p==="template"&&ee&&!ee.provisioning_script?e("p",{class:"field-hint field-wide",children:["This template has no provisioning script, so there are no key-values to pass. Close this dialog and use ",e("strong",{children:"Provisioning"})," on the template row to configure one first."]}):null,se&&p==="template"&&ee?.provisioning_script?e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Provisioning properties (JSON object)"}),e("textarea",{rows:6,spellcheck:!1,value:Q,onInput:_=>g(_.currentTarget.value)}),e("span",{class:"field-hint",children:["Passed to the template provisioning script through",e("code",{children:"ROOKERY_PROVISION_JSON"})," under ",e("code",{children:"properties"}),". Set ",e("code",{children:'"resize_disk": true'})," to extend the root partition and filesystem with the reference script. Leave",e("code",{})," empty to skip provisioning."]})]}):null]}):e(ge,{children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Host block device"}),e("select",{required:!0,value:m,disabled:P,onChange:_=>{let $=_.currentTarget.value;y($);let j=A.find(J=>J.path===$);j&&!r.trim()&&b(ri(j))},children:[e("option",{value:"",children:P?"Loading devices\u2026":"Select a host block device\u2026"}),A.map(_=>e("option",{value:_.path,disabled:!_.available,children:[Va(_.kind)," \xB7 ",_.path," \xB7 ",Ma(_.size_bytes),_.available?"":` \xB7 ${_.unavailable_reason||"unavailable"}`]},`${_.kind}:${_.path}`))]}),e("span",{class:"field-hint",children:"Whole disks, partitions, and logical volumes are listed. Mounted, swap, and otherwise busy devices are disabled."})]}),S?e("p",{class:"form-error field-wide",role:"alert",children:S}):null,de?e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Selected device"}),e("p",{class:"field-hint",children:[Va(de.kind)," \xB7 ",de.path," \xB7"," ",Ma(de.size_bytes),de.model?` \xB7 ${de.model}`:""]}),de.available?null:e("p",{class:"form-error",role:"alert",children:de.unavailable_reason||"This device is not available."})]}):null,A.length===0&&!P&&!S?e("p",{class:"field-hint field-wide",children:"No host block devices were found."}):null]}),e("div",{class:"field field-wide",children:e(Se,{labels:oe,onChange:E,disabled:z})})]}),ue?e("p",{class:"form-error",role:"alert",children:ue}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:z,onClick:me,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:z||(o==="lvm"?t.length===0||p==="template"&&!ee:!de||!de.available),children:z?"Creating\u2026":"Create disk"})]})]})})}function Va(t){switch(t){case"disk":return"Disk";case"partition":return"Partition";case"lv":return"Logical volume";case"raid":return"RAID array";default:return"Block device"}}function Ma(t){let n=Number(t)||0;return n>=1024**4?`${(n/1024**4).toFixed(n%1024**4===0?0:1)} TB`:n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function ri(t){return(t.path.split("/").filter(Boolean).pop()||t.name).replace(/[^A-Za-z0-9._-]+/g,"-").replace(/^[^A-Za-z0-9]+/,"").slice(0,127)||t.name}function Ra({backend:t,onClose:n,onImported:a}){let[l,s]=c([]),[o,f]=c(()=>new Set),[r,b]=c(!0),[w,d]=c(!1),[C,h]=c(""),A=ne(null),I=async()=>{b(!0),h("");try{let L=await V(`/api/storage-backends/${t.id}/importable-disks`);s(Array.isArray(L)?L:[]),f(new Set)}catch(L){s([]),h(L instanceof Error?L.message:String(L))}finally{b(!1)}};G(()=>{A.current?.showModal(),I()},[]);let m=()=>{w||A.current?.close()},y=(L,S)=>{f(N=>{let p=new Set(N);return S?p.add(L):p.delete(L),p})},P=async()=>{if(!(w||o.size===0)){d(!0),h("");try{let L=await V(`/api/storage-backends/${t.id}/import-disks`,{method:"POST",body:JSON.stringify({names:[...o]})});a(Array.isArray(L)?L:[])}catch(L){h(L instanceof Error?L.message:String(L))}finally{d(!1)}}};return e("dialog",{ref:A,id:"import-disk-dialog","aria-labelledby":"import-disk-dialog-title",onClose:n,onClick:L=>{L.target===L.currentTarget&&m()},onCancel:L=>{w&&L.preventDefault()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Storage backend"}),e("h2",{id:"import-disk-dialog-title",children:"Import existing disks"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:m,children:"\xD7"})]}),e("p",{class:"import-disk-lede",children:[t.name," \xB7 ",t.config.vg,"/",t.config.thin_pool," \u2014 select existing thin volumes to register as disks. Importing does not modify LVM, but deleting an imported disk later removes its volume."]}),C?e("div",{class:"banner",role:"alert",children:C}):null,e("div",{class:"import-disk-list",children:[r?e("p",{class:"import-disk-meta",children:"Loading existing thin volumes\u2026"}):null,!r&&l.length===0&&!C?e("div",{class:"empty import-disk-empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A3"}),e("h3",{children:"No thin volumes found"}),e("p",{children:"This storage backend has no existing thin volumes to import."})]}):null,r?null:l.map(L=>e("label",{class:L.registered?"import-disk-row imported":"import-disk-row",children:[e("input",{type:"checkbox",value:L.name,checked:o.has(L.name),disabled:L.registered||w,"aria-label":`Import ${L.name}`,onChange:S=>y(L.name,S.currentTarget.checked)}),e("span",{class:"import-disk-copy",children:[e("span",{class:"import-disk-name",children:L.name}),e("span",{class:"import-disk-meta",children:[L.size_gb," GB \xB7"," ",L.registered?L.attached_vm_name?`already imported \xB7 attached to ${L.attached_vm_name}`:L.storage_backend_name?`already imported \xB7 ${L.storage_backend_name}`:"already imported":"available to import"]})]}),e("span",{class:"import-disk-badge",children:L.registered?"imported":"new"})]},L.name))]}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:r||w,onClick:()=>{I()},children:r?"Refreshing\u2026":"Refresh"}),e("button",{class:"button ghost",type:"button",disabled:w,onClick:m,children:"Cancel"}),e("button",{class:"button primary",type:"button",disabled:w||o.size===0,onClick:()=>{P()},children:w?"Importing\u2026":o.size>0?`Import selected (${o.size})`:"Import selected"})]})]})}var li="[A-Za-z0-9][A-Za-z0-9._\\-]*";function Fa({backend:t,onClose:n,onSaved:a}){let l=t!==null,[s,o]=c(t?.name??""),[f,r]=c(t?.title??""),[b,w]=c(t?.description??""),[d,C]=c(t?.driver??"lvm-thin"),[h,A]=c(t?.config?.vg??""),[I,m]=c(t?.config?.thin_pool??""),[y,P]=c(!!t?.config?.supports_templates),[L,S]=c(t?.config?.preparation_script??""),[N,p]=c(t?.config?.preparation_timeout||"5m"),[T,B]=c(!!t?.config?.allow_template_delete_with_clones),[K,Z]=c(t?.config?.kouja?.endpoint??""),[ie,x]=c(t?.config?.kouja?.protocol??"iscsi"),[R,F]=c(t?.config?.kouja?.storage_class??""),[W,Q]=c(t?.config?.kouja?.initiator??""),[g,oe]=c(t?.config?.kouja?.host_nqn??""),[E,z]=c(t?.config?.kouja?.ca_file??""),[re,ue]=c(t?.config?.kouja?.client_cert_file??""),[v,M]=c(t?.config?.kouja?.client_key_file??""),[k,O]=c(t?.config?.kouja?.server_name??""),[le,se]=c(t?.config?.kouja?.portal??""),[fe,ee]=ke("storage_backend",t?.id),[de,me]=c(!1),[u,_]=c(""),$=ne(null),j=ne(null),J=ne(null);G(()=>{$.current?.showModal(),j.current?.focus()},[]);let pe=()=>{de||$.current?.close()},Le=async q=>{if(q.preventDefault(),!(de||!J.current?.reportValidity())){_(""),me(!0);try{let he=await V(l?`/api/storage-backends/${t?.id}`:"/api/storage-backends",{method:l?"PUT":"POST",body:JSON.stringify({name:s.trim(),title:f.trim(),description:b.trim(),driver:d,config:d==="kouja"?{kouja:{endpoint:K.trim(),protocol:ie,storage_class:R.trim(),initiator:W.trim(),host_nqn:g.trim(),ca_file:E.trim(),client_cert_file:re.trim(),client_key_file:v.trim(),server_name:k.trim(),portal:le.trim()}}:{vg:h.trim(),thin_pool:I.trim(),supports_templates:y,preparation_script:y?L.trim():"",preparation_timeout:y?N.trim()||"5m":"",allow_template_delete_with_clones:y&&T}})});await Ce("storage_backend",he.id,he.name,fe),a(he)}catch(he){_(he instanceof Error?he.message:String(he))}finally{me(!1)}}};return e("dialog",{ref:$,id:"storage-backend-dialog","aria-labelledby":"storage-backend-dialog-title",onClose:n,onClick:q=>{q.target===q.currentTarget&&pe()},onCancel:q=>{de&&q.preventDefault()},children:e("form",{ref:J,novalidate:!0,onSubmit:q=>{Le(q)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:l?"Edit backend":"New backend"}),e("h2",{id:"storage-backend-dialog-title",children:l?`Storage backend \xB7 ${t?.name}`:"Register storage backend"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:pe,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:j,type:"text",required:!0,maxlength:64,pattern:li,autocomplete:"off",spellcheck:!1,placeholder:"fast-pool",value:s,onInput:q=>o(q.currentTarget.value)}),e("span",{class:"field-hint",children:"Used when creating disks."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Title"}),e("input",{type:"text",maxlength:128,autocomplete:"off",placeholder:"Fast NVMe thin pool",value:f,onInput:q=>r(q.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Description ",e("em",{children:"optional"})]}),e("textarea",{rows:2,maxlength:2e3,placeholder:"What this backend is for",value:b,onInput:q=>w(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Driver"}),e("select",{required:!0,value:d,onChange:q=>C(q.currentTarget.value),children:[e("option",{value:"lvm-thin",children:"LVM thin pool"}),e("option",{value:"kouja",children:"Kouja"})]}),e("span",{class:"field-hint",children:"Kouja provisions remote volumes over its agent API."})]}),d==="kouja"?e(ge,{children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Endpoint"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"10.10.0.1:9443",value:K,onInput:q=>Z(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Protocol"}),e("select",{value:ie,onChange:q=>x(q.currentTarget.value),children:[e("option",{value:"iscsi",children:"iSCSI"}),e("option",{value:"nvme-tcp",disabled:!0,children:"NVMe/TCP (coming soon)"})]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Storage class"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"nvme",value:R,onInput:q=>F(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"iSCSI initiator IQN"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"iqn.2026-11.io.example:rookery",value:W,onInput:q=>Q(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["NVMe host NQN ",e("em",{children:"optional"})]}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"nqn.2026-11.io.example:rookery",value:g,onInput:q=>oe(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"CA file"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"/etc/rookery/kouja/ca.crt",value:E,onInput:q=>z(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Client certificate"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"/etc/rookery/kouja/rookery.crt",value:re,onInput:q=>ue(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Client key"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"/etc/rookery/kouja/rookery.key",value:v,onInput:q=>M(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["TLS server name ",e("em",{children:"optional"})]}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"kouja-storage-1",value:k,onInput:q=>O(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["iSCSI portal ",e("em",{children:"optional"})]}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"10.10.0.1:3260",value:le,onInput:q=>se(q.currentTarget.value)})]})]}):null,e("label",{class:"field",hidden:d==="kouja",children:[e("span",{class:"field-label",children:"Volume group"}),e("input",{type:"text",required:d!=="kouja",autocomplete:"off",spellcheck:!1,placeholder:"vg0",value:h,onInput:q=>A(q.currentTarget.value)})]}),e("label",{class:"field",hidden:d==="kouja",children:[e("span",{class:"field-label",children:"Thin pool"}),e("input",{type:"text",required:d!=="kouja",autocomplete:"off",spellcheck:!1,placeholder:"rookery",value:I,onInput:q=>m(q.currentTarget.value)})]}),e("label",{class:"field field-wide checkbox-field",hidden:d==="kouja",children:[e("span",{class:"field-label",children:"Templates"}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:y,onChange:q=>P(q.currentTarget.checked)}),e("span",{children:"Support template disks and template-based disk creation"})]}),e("span",{class:"field-hint",children:"Clone existing template disks with the backend preparation script."})]}),y&&d!=="kouja"?e(ge,{children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Preparation script"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"/usr/local/sbin/rookery-disk-prepare",value:L,onInput:q=>S(q.currentTarget.value)}),e("span",{class:"field-hint",children:"Absolute path. Runs as the API user with disk details in ROOKERY_DISK_JSON."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Script timeout"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"5m",value:N,onInput:q=>p(q.currentTarget.value)})]}),e("label",{class:"field field-wide checkbox-field",children:e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:T,onChange:q=>B(q.currentTarget.checked)}),e("span",{children:"Allow deleting a template while clones still exist"})]})})]}):null,e("div",{class:"field field-wide",children:e(Se,{labels:fe,onChange:ee,disabled:de})})]}),u?e("p",{class:"form-error",role:"alert",children:u}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:de,onClick:pe,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:de,children:de?"Saving\u2026":"Save backend"})]})]})})}function Ba({disk:t,onClose:n,onSaved:a}){let[l,s]=c(t.provisioning_script||""),[o,f]=c(t.provisioning_timeout||""),[r,b]=c(!1),[w,d]=c(""),C=ne(null);G(()=>{C.current?.showModal()},[]);let h=()=>{r||C.current?.close()},A=async I=>{if(I.preventDefault(),!r){b(!0),d("");try{let m=await V(`/api/disks/${t.id}`,{method:"PUT",body:JSON.stringify({provisioning_script:l.trim(),provisioning_timeout:o.trim()})});a(m)}catch(m){d(m instanceof Error?m.message:String(m))}finally{b(!1)}}};return e("dialog",{ref:C,id:"template-provisioning-dialog","aria-labelledby":"template-provisioning-title",onClose:n,onClick:I=>{I.target===I.currentTarget&&h()},onCancel:I=>{r&&I.preventDefault()},children:e("form",{onSubmit:I=>{A(I)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Template provisioning"}),e("h2",{id:"template-provisioning-title",children:["Provisioning \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:h,children:"\xD7"})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Provisioning script"}),e("input",{type:"text",placeholder:"/usr/local/sbin/rookery-provision-template",value:l,onInput:I=>s(I.currentTarget.value)}),e("span",{class:"field-hint",children:["Absolute path to a script run after the backend clones this template. It receives the provisioning context and custom properties in ",e("code",{children:"ROOKERY_PROVISION_JSON"}),". Leave empty to disable provisioning."]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Timeout"}),e("input",{type:"text",placeholder:"5m",value:o,onInput:I=>f(I.currentTarget.value)}),e("span",{class:"field-hint",children:"Examples: 30s, 5m, 1h."})]}),w?e("p",{class:"form-error",role:"alert",children:w}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:r,onClick:h,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:r,children:r?"Saving\u2026":"Save provisioning"})]})]})})}var Ha=new Intl.NumberFormat;function Oa(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function ci(t){let n=Number(t.size_bytes)||0;return n<=0?`${Ha.format(t.size_gb)} GB`:n>=1024**4?`${(n/1024**4).toFixed(n%1024**4===0?0:1)} TB`:n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:`${Ha.format(n)} B`}function Ua(){let[t,n]=c([]),[a,l]=c([]),[s,o]=c(!0),[f,r]=c(""),[b,w]=c(""),[d,C]=c(!1),[h,A]=c(null),[I,m]=c(null),[y,P]=c(null),L=Ne("disk"),S=Ne("storage_backend"),N=ze();G(()=>{P(null)},[N.path,N.search]),G(()=>{let[g,oe,E]=N.segments;if(g==="disks"){C(oe==="new"),A(null),m(null);return}if(g==="storage-backends"){if(C(!1),oe==="new"){A({backend:null}),m(null);return}let z=a.find(re=>String(re.id)===oe);z?(A(E==="edit"?{backend:z}:null),m(E==="import"?z:null)):(A(null),m(null));return}C(!1),A(null),m(null)},[N.path,a]);let p=g=>{n(g),window.dispatchEvent(new CustomEvent("rookery:disks-changed",{detail:g}))},T=async(g=!0)=>{try{let oe=await V("/api/disks");p(Array.isArray(oe)?oe:[]),g&&r("")}catch(oe){g&&r(oe instanceof Error?oe.message:String(oe))}},B=async(g=!0)=>{try{let oe=await V("/api/storage-backends"),E=Array.isArray(oe)?oe:[];E.sort((z,re)=>z.name.localeCompare(re.name,void 0,{sensitivity:"base"})),l(E),g&&w("")}catch(oe){g&&w(oe instanceof Error?oe.message:String(oe))}},K=async()=>{o(!0),await Promise.allSettled([T(),B()]),o(!1)};G(()=>{K();let g=z=>{let re=z.detail;Array.isArray(re)&&n(re)},oe=()=>{te("/disks/new")},E=z=>{z.detail==="disks"&&K()};return window.addEventListener("rookery:disks-data",g),window.addEventListener("rookery:add-disk",oe),window.addEventListener("rookery:tab-changed",E),()=>{window.removeEventListener("rookery:disks-data",g),window.removeEventListener("rookery:add-disk",oe),window.removeEventListener("rookery:tab-changed",E)}},[]),G(()=>{let g=document.getElementById("disk-tab-count");g&&(g.textContent=String(t.length))},[t.length]);let Z=async g=>{if(g.attached_vm_id){ae(`Detach ${g.name} from ${g.attached_vm_name||"its VM"} before deleting it.`,!0);return}if(window.confirm(g.source==="host-device"?`Delete disk "${g.name}"? -This removes only the inventory record. The host block device ${y.device} is not modified.`:`Delete disk "${y.name}"? +This removes only the inventory record. The host block device ${g.device} is not modified.`:g.source==="kouja"?`Delete disk "${g.name}"? -This removes the LVM thin volume and its inventory record.`))try{await V(`/api/disks/${y.id}`,{method:"DELETE"}),p(t.filter(E=>E.id!==y.id)),r(""),oe(`Deleted disk ${y.name}`),H(!1)}catch(E){r(`Could not delete ${y.name}: ${E instanceof Error?E.message:String(E)}`)}},ne=async y=>{if(y.source==="host-device"){oe("Host block devices cannot be templates.",!0);return}if(y.attached_vm_id){oe("Detach the disk from its VM before changing template status.",!0);return}try{let ae=await V(`/api/disks/${y.id}`,{method:"PUT",body:JSON.stringify({is_template:!y.is_template})});p(t.map(E=>E.id===ae.id?ae:E)),oe(ae.is_template?`Marked ${ae.name} as a template`:`Unmarked ${ae.name} as a template`)}catch(ae){r(`Could not update ${y.name}: ${ae instanceof Error?ae.message:String(ae)}`)}},x=async y=>{if(y.disk_count>0){oe("Delete disks using this backend first.",!0);return}if(window.confirm(`Delete storage backend "${y.name}"?`))try{await V(`/api/storage-backends/${y.id}`,{method:"DELETE"}),l(s.filter(E=>E.id!==y.id)),w(""),oe(`Deleted storage backend ${y.name}`)}catch(E){w(`Could not delete ${y.name}: ${E instanceof Error?E.message:String(E)}`)}},R=t.filter(y=>y.attached_vm_id).length,F=t.length===0?a?"Loading disks\u2026":"No disks registered":`${t.length} disk${t.length===1?"":"s"} \xB7 ${R} attached`,W=s.reduce((y,ae)=>y+(ae.disk_count||0),0),Y=s.length===0?a?"Loading storage backends\u2026":"No storage backends registered":`${s.length} backend${s.length===1?"":"s"} \xB7 ${W} disk${W===1?"":"s"}`;return e(ge,{children:[e("section",{class:"panel","aria-labelledby":"storage-backends-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"storage-backends-heading",children:"Storage backends"}),e("p",{class:"panel-subtitle",children:Y})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:a,onClick:()=>{G()},children:a?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>Q("/storage-backends/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add backend"]})]})]}),b?e("div",{class:"banner",role:"alert",children:b}):null,e("div",{class:"table-wrap",children:s.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Driver"}),e("th",{scope:"col",children:"Location"}),e("th",{scope:"col",children:"Disks"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:s.map(y=>{let ae=y.driver==="lvm-thin"&&y.config?.vg&&y.config?.thin_pool;return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:y.name}),y.title||y.description?e("span",{class:"vm-description",title:y.description||y.title||"",children:[y.title,y.description].filter(Boolean).join(" \xB7 ")}):null,e(De,{entityType:"storage_backend",entityID:y.id,name:y.name,labels:S.labels[String(y.id)],onSaved:E=>S.setEntityLabels(String(y.id),E)})]}),e("td",{class:"mono",children:y.driver==="lvm-thin"?"LVM thin":y.driver}),e("td",{class:"mono",children:[y.config?.vg||"\u2014","/",y.config?.thin_pool||"\u2014",y.config?.supports_templates?e("span",{class:"import-disk-badge template-backend-badge",children:"templates"}):null]}),e("td",{class:y.disk_count?"mono":"mono muted",children:String(y.disk_count||0)}),e("td",{class:"updated hide-sm",children:Hs(y.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",disabled:!ae,title:ae?"Import existing thin volumes from this backend.":"Import is available for configured LVM thin backends.",onClick:()=>Q(`/storage-backends/${y.id}/import`),children:"Import"}),e("button",{class:"row-action",type:"button",onClick:()=>Q(`/storage-backends/${y.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",disabled:y.disk_count>0,title:y.disk_count>0?"Delete disks using this backend first.":"",onClick:()=>{x(y)},children:"Delete"})]})})]},y.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A4"}),e("h3",{children:"No storage backends yet"}),e("p",{children:"Register an LVM thin pool before creating disks."}),e("button",{class:"button primary",type:"button",onClick:()=>Q("/storage-backends/new"),children:"Add a storage backend"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"disks-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"disks-heading",children:"Disks"}),e("p",{class:"panel-subtitle",children:F})]}),e("button",{class:"button ghost",type:"button",disabled:a,onClick:()=>{G()},children:a?"Refreshing\u2026":"Refresh"})]}),f?e("div",{class:"banner",role:"alert",children:f}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Size"}),e("th",{scope:"col",children:"Backend"}),e("th",{scope:"col",children:"Attached to"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(y=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:y.name}),y.is_template?e("span",{class:"import-disk-badge",children:"template"}):null,y.source==="host-device"?e("span",{class:"import-disk-badge",children:"passthrough"}):null,y.template_disk_name?e("span",{class:"vm-description",children:["from ",y.template_disk_name]}):null,y.device?e("span",{class:"vm-description",title:y.device,children:y.device}):null,e(De,{entityType:"disk",entityID:y.id,name:y.name,labels:L.labels[String(y.id)],onSaved:ae=>L.setEntityLabels(String(y.id),ae)})]}),e("td",{class:"mono",children:li(y)}),e("td",{class:y.storage_backend_name||y.source==="host-device"?"mono":"mono muted",children:y.source==="host-device"?"host device":y.storage_backend_name||"\u2014"}),e("td",{class:y.is_template?"mono muted":y.attached_vm_id?"mono":"mono muted",children:y.is_template?"template":y.attached_vm_id?y.attached_vm_name||y.attached_vm_id:"unattached"}),e("td",{class:"updated hide-sm",children:Hs(y.updated_at)}),e("td",{children:e("div",{class:"actions",children:[y.is_template?e("button",{class:"row-action",type:"button",onClick:()=>P(y),children:"Provisioning"}):null,y.source==="host-device"?null:e("button",{class:"row-action",type:"button",disabled:!!y.attached_vm_id,title:y.is_template?"Allow this disk to be used as a normal disk again.":"Mark this unattached disk as a template.",onClick:()=>{ne(y)},children:y.is_template?"Unmark":"Template"}),e("button",{class:"row-action danger",type:"button",disabled:!!y.attached_vm_id,title:y.attached_vm_id?`Detach ${y.name} from ${y.attached_vm_name||"its VM"} before deleting it.`:"",onClick:()=>{J(y)},children:"Delete"})]})})]},y.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A3"}),e("h3",{children:"No disks yet"}),e("p",{children:["Create an LVM thin disk here, pass through a host block device, or use"," ",e("strong",{children:"Import"})," on a storage backend to register existing thin volumes."]}),e("button",{class:"button primary",type:"button",onClick:()=>Q("/disks/new"),children:"Create a disk"})]})})]}),g?e(Fs,{disk:g,onClose:()=>P(null),onSaved:y=>{P(null),p(t.map(ae=>ae.id===y.id?y:ae)),oe(`Saved provisioning for ${y.name}`)}}):null,u?e(Ms,{backends:s,disks:t,onClose:()=>{k(!1),Q("/disks",!0)},onSaved:y=>{k(!1),Q("/disks",!0),r(""),oe(`Created disk ${y.name}`),T(),H(!1)}}):null,h?e(Rs,{backend:h.backend,onClose:()=>{A(null),Q("/disks",!0)},onSaved:y=>{A(null),Q("/disks",!0),w(""),oe(`${h.backend?"Saved":"Added"} storage backend ${y.name}`),H(),T(!1)}},h.backend?`edit-${h.backend.id}`:"new"):null,I?e($s,{backend:I,onClose:()=>{m(null),Q("/disks",!0)},onImported:y=>{m(null),Q("/disks",!0),r(""),oe(`Imported ${y.length} disk${y.length===1?"":"s"}`),T(),H(!1)}},`import-${I.id}`):null]})}function Us(t){t&&Fe(e(Os,{}),t)}function di(t){let n=t.response;return n&&typeof n=="object"&&"error"in n&&typeof n.error=="string"?n.error:t.statusText||`Upload failed (${t.status})`}function qs(t,n,s){return new Promise((l,a)=>{let o=new FormData;o.append("kind",t),o.append("file",n,n.name);let f=new XMLHttpRequest;f.open("POST","/api/lightvm/uploads"),f.responseType="json",f.upload.onprogress=r=>{r.lengthComputable&&r.total>0&&s?.(r.loaded/r.total)},f.onerror=()=>a(new Error("Upload failed because the API could not be reached")),f.onabort=()=>a(new Error("Upload was cancelled")),f.onload=()=>{if(f.status>=200&&f.status<300&&f.response){l(f.response);return}a(new Error(di(f)))},f.send(o)})}async function Ws(t){t&&await V(`/api/lightvm/uploads?path=${encodeURIComponent(t)}`,{method:"DELETE"})}var ci={kernel:{endpoint:"/api/lightvm/kernels",entityType:"lightvm_kernel",uploadKind:"kernel",noun:"kernel",namePlaceholder:"vmlinuz-6.12"},initramfs:{endpoint:"/api/lightvm/initramfs",entityType:"lightvm_initramfs",uploadKind:"initramfs",noun:"initramfs",namePlaceholder:"initramfs-6.12"}},ui="[A-Za-z0-9][A-Za-z0-9._\\-]*";function zs(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(1)} MB`:n>=1024?`${(n/1024).toFixed(1)} KB`:`${n} B`}function Gs({kind:t,artifact:n,onClose:s,onSaved:l}){let a=ci[t],o=n!==null,[f,r]=d(n?.name||""),[b,w]=d(n?.description||""),[u,k]=d(null),[h,A]=ke(a.entityType,n?.id),[I,m]=d(!1),[g,P]=d(null),[L,S]=d(""),N=te(null),p=te(null),T=te(null);z(()=>{N.current?.showModal(),p.current?.focus()},[]);let H=()=>{I||N.current?.close()},G=async J=>{if(J.preventDefault(),I||!T.current?.reportValidity())return;if(!o&&!u){S(`Choose an ${a.noun} file to upload.`);return}S(""),m(!0);let ne="";try{let x=n?.path||"",R=n?.file_name||"",F=n?.size_bytes||0;if(u){P(0);let y=await qs(a.uploadKind,u,P);ne=y.path,x=y.path,R=y.file_name,F=y.size_bytes}let W={name:f.trim(),description:b.trim(),path:x,file_name:R},Y=o?await V(`${a.endpoint}/${n.id}`,{method:"PUT",body:JSON.stringify(W)}):await V(a.endpoint,{method:"POST",body:JSON.stringify(W)});ne="",await Ce(a.entityType,Y.id,Y.name,h),l({...Y,size_bytes:Y.size_bytes||F},!o)}catch(x){ne&&await Ws(ne).catch(()=>{}),S(x instanceof Error?x.message:String(x))}finally{m(!1),P(null)}};return e("dialog",{ref:N,id:`${a.uploadKind}-dialog`,"aria-labelledby":`${a.uploadKind}-dialog-title`,onClose:s,onClick:J=>{J.target===J.currentTarget&&H()},onCancel:J=>{I&&J.preventDefault()},children:e("form",{ref:T,novalidate:!0,onSubmit:J=>{G(J)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:o?`Edit ${a.noun}`:`New ${a.noun}`}),e("h2",{id:`${a.uploadKind}-dialog-title`,children:o?`Edit LightVM ${a.noun}`:`Add LightVM ${a.noun}`})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:H,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:p,type:"text",required:!0,maxlength:127,pattern:ui,autocomplete:"off",spellcheck:!1,placeholder:a.namePlaceholder,value:f,onInput:J=>r(J.currentTarget.value)}),e("span",{class:"field-hint",children:["A short label for this ",a.noun," image."]})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Description"}),e("input",{type:"text",maxlength:2e3,autocomplete:"off",placeholder:"Optional notes, version, or source",value:b,onInput:J=>w(J.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:o?`Replace ${a.noun} file`:`${a.noun[0].toUpperCase()}${a.noun.slice(1)} file`}),e("input",{type:"file",required:!o,disabled:I,onChange:J=>k(J.currentTarget.files?.[0]||null)}),e("span",{class:"field-hint",children:o&&n?`Current file: ${n.file_name||n.path} (${zs(n.size_bytes)}). Choose a file only to replace it.`:"The file is uploaded to the Rookery host and stored under the configured LightVM directory."}),u?e("span",{class:"field-hint",children:["Selected: ",u.name," (",zs(u.size),")"]}):null]}),g!==null?e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Uploading"}),e("progress",{max:100,value:Math.round(g*100)}),e("span",{class:"field-hint",children:[Math.round(g*100),"%"]})]}):null,e("div",{class:"field field-wide",children:e(Se,{labels:h,onChange:A,disabled:I})})]}),L?e("p",{class:"form-error",role:"alert",children:L}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:I,onClick:H,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:I,children:I?g!==null?"Uploading\u2026":"Saving\u2026":o?`Save ${a.noun}`:`Add ${a.noun}`})]})]})})}var pi="[A-Za-z0-9][A-Za-z0-9._\\-]*",fi=/^[A-Za-z_][A-Za-z0-9_]*$/,mn="console=hvc0 rdinit=/init";function mi(t){return Array.isArray(t?.consoles)&&t.consoles.length>0?t.consoles.map(n=>({id:n.id,name:n.name})):[{id:0,name:"console"}]}function hi(t){if(t.length===0)return"console";let n=new Set(t.map(l=>l.name.trim().toLowerCase())),s=t.length+1;for(;n.has(`console-${s}`);)s+=1;return`console-${s}`}function bi(t){return Array.isArray(t?.env)?t.env.map(n=>`${n.name}=${n.value}`).join(` -`):""}function gi(t){let n=[],s=new Set,l=t.split(` -`);for(let a=0;a128)return{env:[],error:`Environment variable ${w} on line ${a+1} is too long.`};if(u.length>4096)return{env:[],error:`Environment value for ${w} on line ${a+1} is too long.`};if(u.includes("\0"))return{env:[],error:`Environment value for ${w} on line ${a+1} contains a NUL character.`};if(s.has(w))return{env:[],error:`Environment variable ${w} is duplicated (line ${a+1}).`};s.add(w),n.push({name:w,value:u})}return{env:n,error:""}}function vi(t){return Array.isArray(t?.interfaces)?t.interfaces:[]}function Ks({vm:t,kernels:n,initramfs:s,interfaces:l,onClose:a,onSaved:o}){let f=t!==null,[r,b]=d(t?.name||""),[w,u]=d(t?.description||""),[k,h]=d(String(t?.vcpus??1)),[A,I]=d(String(t?.memory_mb??512)),[m,g]=d(!!t?.console_enabled),[P,L]=d(null),[S,N]=d(t?.cgroup_class||""),[p,T]=d(t?.kernel_id?String(t.kernel_id):""),[H,G]=d(t?.initramfs_id?String(t.initramfs_id):""),[J,ne]=d(t?.image||""),[x,R]=d(()=>bi(t)),[F,W]=d(()=>mi(t)),[Y,y]=d(t?.executable_path||""),[ae,E]=d(Array.isArray(t?.executable_args)?t.executable_args.join(` -`):""),[M,se]=d(t?t.append||"":mn),[ce,v]=d(()=>vi(t)),[$,C]=d(""),[U,de]=d(""),[ie,pe]=ke("lightvm",t?.id),[ee,ue]=d(!1),[me,c]=d(""),_=te(null),B=te(null),X=te(null),Z=new Set(ce.map(q=>q.id)),fe=Array.isArray(P?.classes)?P.classes:[],Ie=fe.filter(q=>!fe.some(we=>we.name.startsWith(q.name+"/"))),le=l.filter(q=>Z.has(q.id)||q.attached_vm_id?!1:!q.attached_lightvm_id||t!==null&&q.attached_lightvm_id===t.id);z(()=>{_.current?.showModal(),B.current?.focus()},[]),z(()=>{V("/api/cgroup-settings").then(L).catch(()=>L(null))},[]),z(()=>{le.length===0&&ce.length===0?de("Create a Rookery Virtual Interface on the Network tab first."):de("Choose an interface and click Add. Interfaces attached to VMs or other LightVMs are hidden.")},[le.length,ce.length]);let be=()=>{ee||_.current?.close()},ve=()=>{let q=Number($);if(!q)return;let we=l.find(Te=>Te.id===q)||ce.find(Te=>Te.id===q);we&&(v(Te=>Te.some(xe=>xe.id===we.id)?Te:[...Te,we]),C(""))},K=q=>{v(we=>we.filter(Te=>Te.id!==q))},$e=(q,we)=>{W(Te=>Te.map((xe,Oe)=>Oe===q?{...xe,name:we}:xe))},O=q=>{W(we=>we.filter((Te,xe)=>xe!==q))},_e=()=>{W(q=>[...q,{id:0,name:hi(q)}])},Ee=async q=>{if(q.preventDefault(),ee||!X.current?.reportValidity())return;let we=Number(p),Te=Number(H);if(!we||!Te){c("Select a kernel and an initramfs image.");return}if(F.length===0){c("Add at least one console.");return}let xe=[],Oe=new Set;for(let Ve of F){let Le=Ve.name.trim();if(!Le){c("Every console needs a name.");return}let Je=Le.toLowerCase();if(Oe.has(Je)){c(`Console ${Le} is duplicated.`);return}Oe.add(Je),xe.push({id:Ve.id,name:Le})}let et=gi(x);if(et.error){c(et.error);return}let Ke=ae.split(` -`).map(Ve=>Ve.replace(/\r$/,"")).filter(Ve=>Ve.length>0),dt={name:r.trim(),description:w.trim(),vcpus:Number(k),memory_mb:Number(A),console_enabled:m,cgroup_class:S,kernel_id:we,initramfs_id:Te,image:J.trim(),env:et.env,executable_path:Y.trim(),executable_args:Ke,append:M.trim(),consoles:xe,interface_ids:ce.map(Ve=>Ve.id)};c(""),ue(!0);try{let Ve=await V(f?`/api/lightvm/instances/${t.id}`:"/api/lightvm/instances",{method:f?"PUT":"POST",body:JSON.stringify(dt)});await Ce("lightvm",Ve.id,Ve.name||r.trim(),ie),o(Ve)}catch(Ve){c(Ve instanceof Error?Ve.message:String(Ve))}finally{ue(!1)}};return e("dialog",{ref:_,id:"lightvm-dialog","aria-labelledby":"lightvm-dialog-title",onClose:a,onClick:q=>{q.target===q.currentTarget&&be()},onCancel:q=>{ee&&q.preventDefault()},children:e("form",{ref:X,novalidate:!0,onSubmit:q=>{Ee(q)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:f?"Edit LightVM":"New LightVM"}),e("h2",{id:"lightvm-dialog-title",children:f?`Edit ${t?.name}`:"Create LightVM"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:be,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:B,type:"text",required:!0,maxlength:64,pattern:pi,autocomplete:"off",spellcheck:!1,placeholder:"app-01",value:r,onInput:q=>b(q.currentTarget.value)}),e("span",{class:"field-hint",children:"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"vCPUs"}),e("input",{type:"number",required:!0,min:1,max:128,step:1,value:k,onInput:q=>h(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Memory (MB)"}),e("input",{type:"number",required:!0,min:128,max:1048576,step:128,value:A,onInput:q=>I(q.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Kernel"}),e("select",{required:!0,value:p,onChange:q=>T(q.currentTarget.value),children:[e("option",{value:"",children:"Select a kernel\u2026"}),n.map(q=>e("option",{value:String(q.id),children:q.name},q.id))]}),e("span",{class:"field-hint",children:n.length===0?"Upload a kernel on the Kernels subtab first.":""})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Initramfs"}),e("select",{required:!0,value:H,onChange:q=>G(q.currentTarget.value),children:[e("option",{value:"",children:"Select an initramfs\u2026"}),s.map(q=>e("option",{value:String(q.id),children:q.name},q.id))]}),e("span",{class:"field-hint",children:s.length===0?"Upload an initramfs on the Initramfs subtab first.":""})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Kernel append"}),e("input",{type:"text",maxlength:4096,autocomplete:"off",spellcheck:!1,placeholder:mn,value:M,onInput:q=>se(q.currentTarget.value)}),e("span",{class:"field-hint",children:["Kernel command line appended at boot. Defaults to ",mn,"."]})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Container image"}),e("input",{type:"text",required:!0,maxlength:512,autocomplete:"off",spellcheck:!1,placeholder:"ghcr.io/example/app:latest",value:J,onInput:q=>ne(q.currentTarget.value)}),e("span",{class:"field-hint",children:"Docker/OCI image reference used by the LightVM runtime."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Executable path"}),e("input",{type:"text",required:!0,maxlength:4096,autocomplete:"off",spellcheck:!1,placeholder:"/usr/local/bin/app",value:Y,onInput:q=>y(q.currentTarget.value)}),e("span",{class:"field-hint",children:"Absolute path to the executable inside the container image."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Arguments"}),e("textarea",{rows:4,spellcheck:!1,placeholder:`--listen -:8080`,value:ae,onInput:q=>E(q.currentTarget.value)}),e("span",{class:"field-hint",children:["One argument per line. Rookery writes the executable path and these arguments to an executable ",e("code",{children:".entrypoint"})," script at the image root."]})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Environment variables"}),e("textarea",{rows:6,spellcheck:!1,autocomplete:"off",placeholder:`APP_ENV=production +This queues the kouja volume for unexport and deletion. The disk stays visible until cleanup finishes.`:`Delete disk "${g.name}"? + +This removes the LVM thin volume and its inventory record.`))try{await V(`/api/disks/${g.id}`,{method:"DELETE"}),g.source==="kouja"?(await T(),ae(`Deletion queued for ${g.name}`)):(p(t.filter(E=>E.id!==g.id)),ae(`Deleted disk ${g.name}`)),r(""),B(!1)}catch(E){r(`Could not delete ${g.name}: ${E instanceof Error?E.message:String(E)}`)}},ie=async g=>{if(g.source!=="lvm-thin"&&g.source){ae("Only LVM thin disks can be templates.",!0);return}if(g.attached_vm_id){ae("Detach the disk from its VM before changing template status.",!0);return}try{let oe=await V(`/api/disks/${g.id}`,{method:"PUT",body:JSON.stringify({is_template:!g.is_template})});p(t.map(E=>E.id===oe.id?oe:E)),ae(oe.is_template?`Marked ${oe.name} as a template`:`Unmarked ${oe.name} as a template`)}catch(oe){r(`Could not update ${g.name}: ${oe instanceof Error?oe.message:String(oe)}`)}},x=async g=>{if(g.disk_count>0){ae("Delete disks using this backend first.",!0);return}if(window.confirm(`Delete storage backend "${g.name}"?`))try{await V(`/api/storage-backends/${g.id}`,{method:"DELETE"}),l(a.filter(E=>E.id!==g.id)),w(""),ae(`Deleted storage backend ${g.name}`)}catch(E){w(`Could not delete ${g.name}: ${E instanceof Error?E.message:String(E)}`)}},R=t.filter(g=>g.attached_vm_id).length,F=t.length===0?s?"Loading disks\u2026":"No disks registered":`${t.length} disk${t.length===1?"":"s"} \xB7 ${R} attached`,W=a.reduce((g,oe)=>g+(oe.disk_count||0),0),Q=a.length===0?s?"Loading storage backends\u2026":"No storage backends registered":`${a.length} backend${a.length===1?"":"s"} \xB7 ${W} disk${W===1?"":"s"}`;return e(ge,{children:[e("section",{class:"panel","aria-labelledby":"storage-backends-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"storage-backends-heading",children:"Storage backends"}),e("p",{class:"panel-subtitle",children:Q})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:s,onClick:()=>{K()},children:s?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>te("/storage-backends/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add backend"]})]})]}),b?e("div",{class:"banner",role:"alert",children:b}):null,e("div",{class:"table-wrap",children:a.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Driver"}),e("th",{scope:"col",children:"Location"}),e("th",{scope:"col",children:"Disks"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:a.map(g=>{let oe=g.driver==="lvm-thin"&&g.config?.vg&&g.config?.thin_pool;return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:g.name}),g.title||g.description?e("span",{class:"vm-description",title:g.description||g.title||"",children:[g.title,g.description].filter(Boolean).join(" \xB7 ")}):null,e(De,{entityType:"storage_backend",entityID:g.id,name:g.name,labels:S.labels[String(g.id)],onSaved:E=>S.setEntityLabels(String(g.id),E)})]}),e("td",{class:"mono",children:g.driver==="lvm-thin"?"LVM thin":g.driver==="kouja"?"Kouja":g.driver}),e("td",{class:"mono",children:[g.driver==="kouja"?`${g.config?.kouja?.endpoint||"\u2014"} \xB7 ${g.config?.kouja?.storage_class||"\u2014"}`:`${g.config?.vg||"\u2014"}/${g.config?.thin_pool||"\u2014"}`,g.config?.supports_templates?e("span",{class:"import-disk-badge template-backend-badge",children:"templates"}):null]}),e("td",{class:g.disk_count?"mono":"mono muted",children:String(g.disk_count||0)}),e("td",{class:"updated hide-sm",children:Oa(g.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",disabled:!oe,title:oe?"Import existing thin volumes from this backend.":"Import is available for configured LVM thin backends.",onClick:()=>te(`/storage-backends/${g.id}/import`),children:"Import"}),e("button",{class:"row-action",type:"button",onClick:()=>te(`/storage-backends/${g.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",disabled:g.disk_count>0,title:g.disk_count>0?"Delete disks using this backend first.":"",onClick:()=>{x(g)},children:"Delete"})]})})]},g.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A4"}),e("h3",{children:"No storage backends yet"}),e("p",{children:"Register an LVM thin pool before creating disks."}),e("button",{class:"button primary",type:"button",onClick:()=>te("/storage-backends/new"),children:"Add a storage backend"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"disks-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"disks-heading",children:"Disks"}),e("p",{class:"panel-subtitle",children:F})]}),e("button",{class:"button ghost",type:"button",disabled:s,onClick:()=>{K()},children:s?"Refreshing\u2026":"Refresh"})]}),f?e("div",{class:"banner",role:"alert",children:f}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Size"}),e("th",{scope:"col",children:"Backend"}),e("th",{scope:"col",children:"Attached to"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(g=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:g.name}),g.is_template?e("span",{class:"import-disk-badge",children:"template"}):null,g.source==="host-device"?e("span",{class:"import-disk-badge",children:"passthrough"}):null,g.source==="kouja"?e("span",{class:"import-disk-badge",children:"kouja"}):null,g.deletion_state?e("span",{class:"import-disk-badge",title:g.deletion_error||g.deletion_state,children:g.deletion_state==="delete_failed"?"delete failed":g.deletion_state}):null,g.template_disk_name?e("span",{class:"vm-description",children:["from ",g.template_disk_name]}):null,g.locator?e("span",{class:"vm-description",title:g.locator,children:g.locator}):g.device?e("span",{class:"vm-description",title:g.device,children:g.device}):null,e(De,{entityType:"disk",entityID:g.id,name:g.name,labels:L.labels[String(g.id)],onSaved:oe=>L.setEntityLabels(String(g.id),oe)})]}),e("td",{class:"mono",children:ci(g)}),e("td",{class:g.storage_backend_name||g.source==="host-device"?"mono":"mono muted",children:g.source==="host-device"?"host device":g.storage_backend_name||"\u2014"}),e("td",{class:g.is_template?"mono muted":g.attached_vm_id?"mono":"mono muted",children:g.is_template?"template":g.attached_vm_id?g.attached_vm_name||g.attached_vm_id:"unattached"}),e("td",{class:"updated hide-sm",children:Oa(g.updated_at)}),e("td",{children:e("div",{class:"actions",children:[g.is_template?e("button",{class:"row-action",type:"button",onClick:()=>P(g),children:"Provisioning"}):null,g.source==="host-device"||g.source&&g.source!=="lvm-thin"?null:e("button",{class:"row-action",type:"button",disabled:!!g.attached_vm_id,title:g.is_template?"Allow this disk to be used as a normal disk again.":"Mark this unattached disk as a template.",onClick:()=>{ie(g)},children:g.is_template?"Unmark":"Template"}),e("button",{class:"row-action danger",type:"button",disabled:!!g.attached_vm_id,title:g.attached_vm_id?`Detach ${g.name} from ${g.attached_vm_name||"its VM"} before deleting it.`:"",onClick:()=>{Z(g)},children:"Delete"})]})})]},g.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A3"}),e("h3",{children:"No disks yet"}),e("p",{children:["Create an LVM thin disk here, pass through a host block device, or use"," ",e("strong",{children:"Import"})," on a storage backend to register existing thin volumes."]}),e("button",{class:"button primary",type:"button",onClick:()=>te("/disks/new"),children:"Create a disk"})]})})]}),y?e(Ba,{disk:y,onClose:()=>P(null),onSaved:g=>{P(null),p(t.map(oe=>oe.id===g.id?g:oe)),ae(`Saved provisioning for ${g.name}`)}}):null,d?e($a,{backends:a,disks:t,onClose:()=>{C(!1),te("/disks",!0)},onSaved:g=>{C(!1),te("/disks",!0),r(""),ae(`Created disk ${g.name}`),T(),B(!1)}}):null,h?e(Fa,{backend:h.backend,onClose:()=>{A(null),te("/disks",!0)},onSaved:g=>{A(null),te("/disks",!0),w(""),ae(`${h.backend?"Saved":"Added"} storage backend ${g.name}`),B(),T(!1)}},h.backend?`edit-${h.backend.id}`:"new"):null,I?e(Ra,{backend:I,onClose:()=>{m(null),te("/disks",!0)},onImported:g=>{m(null),te("/disks",!0),r(""),ae(`Imported ${g.length} disk${g.length===1?"":"s"}`),T(),B(!1)}},`import-${I.id}`):null]})}function qa(t){t&&Fe(e(Ua,{}),t)}function di(t){let n=t.response;return n&&typeof n=="object"&&"error"in n&&typeof n.error=="string"?n.error:t.statusText||`Upload failed (${t.status})`}function Wa(t,n,a){return new Promise((l,s)=>{let o=new FormData;o.append("kind",t),o.append("file",n,n.name);let f=new XMLHttpRequest;f.open("POST","/api/lightvm/uploads"),f.responseType="json",f.upload.onprogress=r=>{r.lengthComputable&&r.total>0&&a?.(r.loaded/r.total)},f.onerror=()=>s(new Error("Upload failed because the API could not be reached")),f.onabort=()=>s(new Error("Upload was cancelled")),f.onload=()=>{if(f.status>=200&&f.status<300&&f.response){l(f.response);return}s(new Error(di(f)))},f.send(o)})}async function za(t){t&&await V(`/api/lightvm/uploads?path=${encodeURIComponent(t)}`,{method:"DELETE"})}var ui={kernel:{endpoint:"/api/lightvm/kernels",entityType:"lightvm_kernel",uploadKind:"kernel",noun:"kernel",namePlaceholder:"vmlinuz-6.12"},initramfs:{endpoint:"/api/lightvm/initramfs",entityType:"lightvm_initramfs",uploadKind:"initramfs",noun:"initramfs",namePlaceholder:"initramfs-6.12"}},pi="[A-Za-z0-9][A-Za-z0-9._\\-]*";function Ga(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(1)} MB`:n>=1024?`${(n/1024).toFixed(1)} KB`:`${n} B`}function Ka({kind:t,artifact:n,onClose:a,onSaved:l}){let s=ui[t],o=n!==null,[f,r]=c(n?.name||""),[b,w]=c(n?.description||""),[d,C]=c(null),[h,A]=ke(s.entityType,n?.id),[I,m]=c(!1),[y,P]=c(null),[L,S]=c(""),N=ne(null),p=ne(null),T=ne(null);G(()=>{N.current?.showModal(),p.current?.focus()},[]);let B=()=>{I||N.current?.close()},K=async Z=>{if(Z.preventDefault(),I||!T.current?.reportValidity())return;if(!o&&!d){S(`Choose an ${s.noun} file to upload.`);return}S(""),m(!0);let ie="";try{let x=n?.path||"",R=n?.file_name||"",F=n?.size_bytes||0;if(d){P(0);let g=await Wa(s.uploadKind,d,P);ie=g.path,x=g.path,R=g.file_name,F=g.size_bytes}let W={name:f.trim(),description:b.trim(),path:x,file_name:R},Q=o?await V(`${s.endpoint}/${n.id}`,{method:"PUT",body:JSON.stringify(W)}):await V(s.endpoint,{method:"POST",body:JSON.stringify(W)});ie="",await Ce(s.entityType,Q.id,Q.name,h),l({...Q,size_bytes:Q.size_bytes||F},!o)}catch(x){ie&&await za(ie).catch(()=>{}),S(x instanceof Error?x.message:String(x))}finally{m(!1),P(null)}};return e("dialog",{ref:N,id:`${s.uploadKind}-dialog`,"aria-labelledby":`${s.uploadKind}-dialog-title`,onClose:a,onClick:Z=>{Z.target===Z.currentTarget&&B()},onCancel:Z=>{I&&Z.preventDefault()},children:e("form",{ref:T,novalidate:!0,onSubmit:Z=>{K(Z)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:o?`Edit ${s.noun}`:`New ${s.noun}`}),e("h2",{id:`${s.uploadKind}-dialog-title`,children:o?`Edit LightVM ${s.noun}`:`Add LightVM ${s.noun}`})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:B,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:p,type:"text",required:!0,maxlength:127,pattern:pi,autocomplete:"off",spellcheck:!1,placeholder:s.namePlaceholder,value:f,onInput:Z=>r(Z.currentTarget.value)}),e("span",{class:"field-hint",children:["A short label for this ",s.noun," image."]})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Description"}),e("input",{type:"text",maxlength:2e3,autocomplete:"off",placeholder:"Optional notes, version, or source",value:b,onInput:Z=>w(Z.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:o?`Replace ${s.noun} file`:`${s.noun[0].toUpperCase()}${s.noun.slice(1)} file`}),e("input",{type:"file",required:!o,disabled:I,onChange:Z=>C(Z.currentTarget.files?.[0]||null)}),e("span",{class:"field-hint",children:o&&n?`Current file: ${n.file_name||n.path} (${Ga(n.size_bytes)}). Choose a file only to replace it.`:"The file is uploaded to the Rookery host and stored under the configured LightVM directory."}),d?e("span",{class:"field-hint",children:["Selected: ",d.name," (",Ga(d.size),")"]}):null]}),y!==null?e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Uploading"}),e("progress",{max:100,value:Math.round(y*100)}),e("span",{class:"field-hint",children:[Math.round(y*100),"%"]})]}):null,e("div",{class:"field field-wide",children:e(Se,{labels:h,onChange:A,disabled:I})})]}),L?e("p",{class:"form-error",role:"alert",children:L}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:I,onClick:B,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:I,children:I?y!==null?"Uploading\u2026":"Saving\u2026":o?`Save ${s.noun}`:`Add ${s.noun}`})]})]})})}var fi="[A-Za-z0-9][A-Za-z0-9._\\-]*",mi=/^[A-Za-z_][A-Za-z0-9_]*$/,hn="console=hvc0 rdinit=/init";function hi(t){return Array.isArray(t?.consoles)&&t.consoles.length>0?t.consoles.map(n=>({id:n.id,name:n.name})):[{id:0,name:"console"}]}function bi(t){if(t.length===0)return"console";let n=new Set(t.map(l=>l.name.trim().toLowerCase())),a=t.length+1;for(;n.has(`console-${a}`);)a+=1;return`console-${a}`}function gi(t){return Array.isArray(t?.env)?t.env.map(n=>`${n.name}=${n.value}`).join(` +`):""}function vi(t){let n=[],a=new Set,l=t.split(` +`);for(let s=0;s128)return{env:[],error:`Environment variable ${w} on line ${s+1} is too long.`};if(d.length>4096)return{env:[],error:`Environment value for ${w} on line ${s+1} is too long.`};if(d.includes("\0"))return{env:[],error:`Environment value for ${w} on line ${s+1} contains a NUL character.`};if(a.has(w))return{env:[],error:`Environment variable ${w} is duplicated (line ${s+1}).`};a.add(w),n.push({name:w,value:d})}return{env:n,error:""}}function yi(t){return Array.isArray(t?.interfaces)?t.interfaces:[]}function ja({vm:t,kernels:n,initramfs:a,interfaces:l,onClose:s,onSaved:o}){let f=t!==null,[r,b]=c(t?.name||""),[w,d]=c(t?.description||""),[C,h]=c(String(t?.vcpus??1)),[A,I]=c(String(t?.memory_mb??512)),[m,y]=c(!!t?.console_enabled),[P,L]=c(null),[S,N]=c(t?.cgroup_class||""),[p,T]=c(t?.kernel_id?String(t.kernel_id):""),[B,K]=c(t?.initramfs_id?String(t.initramfs_id):""),[Z,ie]=c(t?.image||""),[x,R]=c(()=>gi(t)),[F,W]=c(()=>hi(t)),[Q,g]=c(t?.executable_path||""),[oe,E]=c(Array.isArray(t?.executable_args)?t.executable_args.join(` +`):""),[z,re]=c(t?t.append||"":hn),[ue,v]=c(()=>yi(t)),[M,k]=c(""),[O,le]=c(""),[se,fe]=ke("lightvm",t?.id),[ee,de]=c(!1),[me,u]=c(""),_=ne(null),$=ne(null),j=ne(null),J=new Set(ue.map(U=>U.id)),pe=Array.isArray(P?.classes)?P.classes:[],Le=pe.filter(U=>!pe.some(we=>we.name.startsWith(U.name+"/"))),q=l.filter(U=>J.has(U.id)||U.attached_vm_id?!1:!U.attached_lightvm_id||t!==null&&U.attached_lightvm_id===t.id);G(()=>{_.current?.showModal(),$.current?.focus()},[]),G(()=>{V("/api/cgroup-settings").then(L).catch(()=>L(null))},[]),G(()=>{q.length===0&&ue.length===0?le("Create a Rookery Virtual Interface on the Network tab first."):le("Choose an interface and click Add. Interfaces attached to VMs or other LightVMs are hidden.")},[q.length,ue.length]);let he=()=>{ee||_.current?.close()},ve=()=>{let U=Number(M);if(!U)return;let we=l.find(Te=>Te.id===U)||ue.find(Te=>Te.id===U);we&&(v(Te=>Te.some(xe=>xe.id===we.id)?Te:[...Te,we]),k(""))},X=U=>{v(we=>we.filter(Te=>Te.id!==U))},$e=(U,we)=>{W(Te=>Te.map((xe,Oe)=>Oe===U?{...xe,name:we}:xe))},H=U=>{W(we=>we.filter((Te,xe)=>xe!==U))},_e=()=>{W(U=>[...U,{id:0,name:bi(U)}])},Ee=async U=>{if(U.preventDefault(),ee||!j.current?.reportValidity())return;let we=Number(p),Te=Number(B);if(!we||!Te){u("Select a kernel and an initramfs image.");return}if(F.length===0){u("Add at least one console.");return}let xe=[],Oe=new Set;for(let Ve of F){let Ie=Ve.name.trim();if(!Ie){u("Every console needs a name.");return}let je=Ie.toLowerCase();if(Oe.has(je)){u(`Console ${Ie} is duplicated.`);return}Oe.add(je),xe.push({id:Ve.id,name:Ie})}let et=vi(x);if(et.error){u(et.error);return}let Ke=oe.split(` +`).map(Ve=>Ve.replace(/\r$/,"")).filter(Ve=>Ve.length>0),ct={name:r.trim(),description:w.trim(),vcpus:Number(C),memory_mb:Number(A),console_enabled:m,cgroup_class:S,kernel_id:we,initramfs_id:Te,image:Z.trim(),env:et.env,executable_path:Q.trim(),executable_args:Ke,append:z.trim(),consoles:xe,interface_ids:ue.map(Ve=>Ve.id)};u(""),de(!0);try{let Ve=await V(f?`/api/lightvm/instances/${t.id}`:"/api/lightvm/instances",{method:f?"PUT":"POST",body:JSON.stringify(ct)});await Ce("lightvm",Ve.id,Ve.name||r.trim(),se),o(Ve)}catch(Ve){u(Ve instanceof Error?Ve.message:String(Ve))}finally{de(!1)}};return e("dialog",{ref:_,id:"lightvm-dialog","aria-labelledby":"lightvm-dialog-title",onClose:s,onClick:U=>{U.target===U.currentTarget&&he()},onCancel:U=>{ee&&U.preventDefault()},children:e("form",{ref:j,novalidate:!0,onSubmit:U=>{Ee(U)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:f?"Edit LightVM":"New LightVM"}),e("h2",{id:"lightvm-dialog-title",children:f?`Edit ${t?.name}`:"Create LightVM"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:he,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:$,type:"text",required:!0,maxlength:64,pattern:fi,autocomplete:"off",spellcheck:!1,placeholder:"app-01",value:r,onInput:U=>b(U.currentTarget.value)}),e("span",{class:"field-hint",children:"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"vCPUs"}),e("input",{type:"number",required:!0,min:1,max:128,step:1,value:C,onInput:U=>h(U.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Memory (MB)"}),e("input",{type:"number",required:!0,min:128,max:1048576,step:128,value:A,onInput:U=>I(U.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Kernel"}),e("select",{required:!0,value:p,onChange:U=>T(U.currentTarget.value),children:[e("option",{value:"",children:"Select a kernel\u2026"}),n.map(U=>e("option",{value:String(U.id),children:U.name},U.id))]}),e("span",{class:"field-hint",children:n.length===0?"Upload a kernel on the Kernels subtab first.":""})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Initramfs"}),e("select",{required:!0,value:B,onChange:U=>K(U.currentTarget.value),children:[e("option",{value:"",children:"Select an initramfs\u2026"}),a.map(U=>e("option",{value:String(U.id),children:U.name},U.id))]}),e("span",{class:"field-hint",children:a.length===0?"Upload an initramfs on the Initramfs subtab first.":""})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Kernel append"}),e("input",{type:"text",maxlength:4096,autocomplete:"off",spellcheck:!1,placeholder:hn,value:z,onInput:U=>re(U.currentTarget.value)}),e("span",{class:"field-hint",children:["Kernel command line appended at boot. Defaults to ",hn,"."]})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Container image"}),e("input",{type:"text",required:!0,maxlength:512,autocomplete:"off",spellcheck:!1,placeholder:"ghcr.io/example/app:latest",value:Z,onInput:U=>ie(U.currentTarget.value)}),e("span",{class:"field-hint",children:"Docker/OCI image reference used by the LightVM runtime."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Executable path"}),e("input",{type:"text",required:!0,maxlength:4096,autocomplete:"off",spellcheck:!1,placeholder:"/usr/local/bin/app",value:Q,onInput:U=>g(U.currentTarget.value)}),e("span",{class:"field-hint",children:"Absolute path to the executable inside the container image."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Arguments"}),e("textarea",{rows:4,spellcheck:!1,placeholder:`--listen +:8080`,value:oe,onInput:U=>E(U.currentTarget.value)}),e("span",{class:"field-hint",children:["One argument per line. Rookery writes the executable path and these arguments to an executable ",e("code",{children:".entrypoint"})," script at the image root."]})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Environment variables"}),e("textarea",{rows:6,spellcheck:!1,autocomplete:"off",placeholder:`APP_ENV=production DATABASE_URL=postgres://db/app -LOG_LEVEL=info`,value:x,onInput:q=>R(q.currentTarget.value)}),e("span",{class:"field-hint",children:["One ",e("code",{children:"NAME=value"})," per line. Rookery writes these to"," ",e("code",{children:".env"})," at the root of the mounted image before starting the LightVM. Blank lines, ",e("code",{children:"export "})," prefixes, and lines beginning with ",e("code",{children:"#"})," are ignored."]})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Consoles"}),e("div",{class:"console-editor",children:[F.map((q,we)=>e("div",{class:"console-row",children:[e("input",{type:"text",required:!0,maxlength:64,autocomplete:"off",spellcheck:!1,placeholder:we===0?"console":`console-${we+1}`,"aria-label":`Console name ${we+1}`,value:q.name,onInput:Te=>$e(we,Te.currentTarget.value)}),e("button",{class:"row-action danger",type:"button",disabled:F.length<=1,title:F.length<=1?"A LightVM must have at least one console.":"",onClick:()=>O(we),children:"Remove"})]},`${q.id}-${we}`)),e("div",{children:e("button",{class:"button ghost",type:"button",disabled:F.length>=32,onClick:_e,children:"Add console"})})]}),e("span",{class:"field-hint",children:["The first console is exposed as ",e("code",{children:"org.example.console"}),"; additional ports use ",e("code",{children:"org.example.console.N"}),". Open the terminal page to choose which console to use."]})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"field-label",children:"Console access"}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:m,onChange:q=>g(q.currentTarget.checked)}),e("span",{children:"Allow end users to open this LightVM's consoles"})]}),e("span",{class:"field-hint",children:"Applies to every console port; disabling it closes existing console sessions."})]}),e("div",{class:"field field-wide",children:e("label",{class:"field",children:[e("span",{class:"field-label",children:["Cgroup class ",e("em",{children:"optional"})]}),e("select",{value:S,onChange:q=>N(q.currentTarget.value),children:[e("option",{value:"",children:"Shared cgroup (host defaults)"}),!Ie.some(q=>q.name===S)&&S?e("option",{value:S,children:[S," (not a leaf class)"]}):null,Ie.map(q=>e("option",{value:q.name,children:q.name},q.name))]}),e("span",{class:"field-hint",children:["Select a leaf class defined in Settings.",P&&!P.enabled?" Host-wide cgroup limits are disabled in Settings; the class applies when they are enabled.":""]})]})}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Network interfaces ",e("em",{children:"optional"})]}),e("div",{class:"interface-picker",children:[e("select",{value:$,disabled:le.length===0,onChange:q=>C(q.currentTarget.value),children:[e("option",{value:"",children:le.length>0?"Select an interface\u2026":"No interfaces available"}),le.map(q=>e("option",{value:String(q.id),children:[q.name," \xB7 ",q.network_name]},q.id))]}),e("button",{class:"button ghost",type:"button",disabled:le.length===0,onClick:ve,children:"Add"})]}),e("ul",{class:"interface-list",children:ce.length===0?e("li",{class:"empty",children:"No interfaces selected"}):ce.map(q=>e("li",{children:[e("span",{class:"interface-list-name",children:[q.name,e("span",{class:"interface-list-network",children:q.network_name})]}),e("button",{class:"row-action danger",type:"button",onClick:()=>K(q.id),children:"Remove"})]},q.id))}),e("span",{class:"field-hint",children:U})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Description ",e("em",{children:"optional"})]}),e("textarea",{rows:3,maxlength:2e3,placeholder:"What is this LightVM for?",value:w,onInput:q=>u(q.currentTarget.value)})]}),e("div",{class:"field field-wide",children:e(Se,{labels:ie,onChange:pe,disabled:ee})})]}),me?e("p",{class:"form-error",role:"alert",children:me}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:ee,onClick:be,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:ee,children:ee?f?"Saving\u2026":"Creating\u2026":f?"Save changes":"Create LightVM"})]})]})})}var Js=[{id:"kernels",label:"Kernels"},{id:"initramfs",label:"Initramfs"},{id:"instances",label:"LightVMs"}];function Xs(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function hn(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function st(t){return[...t].sort((n,s)=>n.name.localeCompare(s.name,void 0,{sensitivity:"base"}))}function Zs(t){return t==="kernel"?"kernels":"initramfs"}function yi(t){return t==="kernel"?"/api/lightvm/kernels":"/api/lightvm/initramfs"}function js(){let[t,n]=d([]),[s,l]=d([]),[a,o]=d([]),[f,r]=d([]),[b,w]=d("kernels"),[u,k]=d(!0),[h,A]=d(""),[I,m]=d(""),[g,P]=d(""),[L,S]=d(""),[N,p]=d(null),[T,H]=d(null),[G,J]=d(null),ne=Ne("lightvm_kernel"),x=Ne("lightvm_initramfs"),R=Ne("lightvm"),F=ze(),W=c=>{n(c),window.dispatchEvent(new CustomEvent("rookery:lightvm-kernels-changed",{detail:c}))},Y=c=>{l(c),window.dispatchEvent(new CustomEvent("rookery:lightvm-initramfs-changed",{detail:c}))},y=c=>{o(c),window.dispatchEvent(new CustomEvent("rookery:lightvm-instances-changed",{detail:c}))},ae=async(c=!0)=>{try{let _=await V("/api/lightvm/kernels");W(st(Array.isArray(_)?_:[])),c&&A("")}catch(_){c&&A(_ instanceof Error?_.message:String(_))}},E=async(c=!0)=>{try{let _=await V("/api/lightvm/initramfs");Y(st(Array.isArray(_)?_:[])),c&&m("")}catch(_){c&&m(_ instanceof Error?_.message:String(_))}},M=async(c=!0)=>{try{let _=await V("/api/lightvm/instances");y(st(Array.isArray(_)?_:[])),c&&P("")}catch(_){c&&P(_ instanceof Error?_.message:String(_))}},se=async()=>{try{let c=await V("/api/interfaces");r(Array.isArray(c)?c:[])}catch{r([])}},ce=async()=>{try{let c=await V("/api/status");p(c.lightvm??null)}catch{p(null)}},v=async()=>{k(!0),await Promise.allSettled([ae(),E(),M(),se(),ce()]),k(!1)};z(()=>{v();let c=Z=>{Z.detail==="lightvm"&&v()},_=()=>Q("/lightvm/kernels/new"),B=()=>Q("/lightvm/initramfs/new"),X=()=>Q("/lightvm/instances/new");return window.addEventListener("rookery:tab-changed",c),window.addEventListener("rookery:add-lightvm-kernel",_),window.addEventListener("rookery:add-lightvm-initramfs",B),window.addEventListener("rookery:add-lightvm-instance",X),()=>{window.removeEventListener("rookery:tab-changed",c),window.removeEventListener("rookery:add-lightvm-kernel",_),window.removeEventListener("rookery:add-lightvm-initramfs",B),window.removeEventListener("rookery:add-lightvm-instance",X)}},[]),z(()=>{let[c,_,B,X]=F.segments;if(c!=="lightvm"){H(null),J(null);return}if(_&&Js.some(Z=>Z.id===_)?w(_):w("kernels"),_==="instances"){if(H(null),B==="new"){J({vm:null});return}if(B&&X==="edit"){let Z=a.find(fe=>fe.id===B);if(Z){J({vm:Z});return}}J(null);return}if(J(null),(_==="kernels"||_==="initramfs")&&B==="new"){H({kind:_==="kernels"?"kernel":"initramfs",artifact:null});return}if(B&&X==="edit"&&(_==="kernels"||_==="initramfs")){let fe=(_==="kernels"?t:s).find(Ie=>String(Ie.id)===B);if(fe){H({kind:_==="kernels"?"kernel":"initramfs",artifact:fe});return}}H(null)},[F.path,t,s,a]),z(()=>{let c=document.getElementById("lightvm-tab-count");c&&(c.textContent=String(a.length))},[a.length]);let $=async(c,_)=>{let B=c==="kernel"?"kernel":"initramfs",X=_.managed?"The uploaded file is deleted from the Rookery host.":"The file on the host is left in place.";if(window.confirm(`Delete ${B} "${_.name}"? +LOG_LEVEL=info`,value:x,onInput:U=>R(U.currentTarget.value)}),e("span",{class:"field-hint",children:["One ",e("code",{children:"NAME=value"})," per line. Rookery writes these to"," ",e("code",{children:".env"})," at the root of the mounted image before starting the LightVM. Blank lines, ",e("code",{children:"export "})," prefixes, and lines beginning with ",e("code",{children:"#"})," are ignored."]})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Consoles"}),e("div",{class:"console-editor",children:[F.map((U,we)=>e("div",{class:"console-row",children:[e("input",{type:"text",required:!0,maxlength:64,autocomplete:"off",spellcheck:!1,placeholder:we===0?"console":`console-${we+1}`,"aria-label":`Console name ${we+1}`,value:U.name,onInput:Te=>$e(we,Te.currentTarget.value)}),e("button",{class:"row-action danger",type:"button",disabled:F.length<=1,title:F.length<=1?"A LightVM must have at least one console.":"",onClick:()=>H(we),children:"Remove"})]},`${U.id}-${we}`)),e("div",{children:e("button",{class:"button ghost",type:"button",disabled:F.length>=32,onClick:_e,children:"Add console"})})]}),e("span",{class:"field-hint",children:["The first console is exposed as ",e("code",{children:"org.example.console"}),"; additional ports use ",e("code",{children:"org.example.console.N"}),". Open the terminal page to choose which console to use."]})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"field-label",children:"Console access"}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:m,onChange:U=>y(U.currentTarget.checked)}),e("span",{children:"Allow end users to open this LightVM's consoles"})]}),e("span",{class:"field-hint",children:"Applies to every console port; disabling it closes existing console sessions."})]}),e("div",{class:"field field-wide",children:e("label",{class:"field",children:[e("span",{class:"field-label",children:["Cgroup class ",e("em",{children:"optional"})]}),e("select",{value:S,onChange:U=>N(U.currentTarget.value),children:[e("option",{value:"",children:"Shared cgroup (host defaults)"}),!Le.some(U=>U.name===S)&&S?e("option",{value:S,children:[S," (not a leaf class)"]}):null,Le.map(U=>e("option",{value:U.name,children:U.name},U.name))]}),e("span",{class:"field-hint",children:["Select a leaf class defined in Settings.",P&&!P.enabled?" Host-wide cgroup limits are disabled in Settings; the class applies when they are enabled.":""]})]})}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Network interfaces ",e("em",{children:"optional"})]}),e("div",{class:"interface-picker",children:[e("select",{value:M,disabled:q.length===0,onChange:U=>k(U.currentTarget.value),children:[e("option",{value:"",children:q.length>0?"Select an interface\u2026":"No interfaces available"}),q.map(U=>e("option",{value:String(U.id),children:[U.name," \xB7 ",U.network_name]},U.id))]}),e("button",{class:"button ghost",type:"button",disabled:q.length===0,onClick:ve,children:"Add"})]}),e("ul",{class:"interface-list",children:ue.length===0?e("li",{class:"empty",children:"No interfaces selected"}):ue.map(U=>e("li",{children:[e("span",{class:"interface-list-name",children:[U.name,e("span",{class:"interface-list-network",children:U.network_name})]}),e("button",{class:"row-action danger",type:"button",onClick:()=>X(U.id),children:"Remove"})]},U.id))}),e("span",{class:"field-hint",children:O})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Description ",e("em",{children:"optional"})]}),e("textarea",{rows:3,maxlength:2e3,placeholder:"What is this LightVM for?",value:w,onInput:U=>d(U.currentTarget.value)})]}),e("div",{class:"field field-wide",children:e(Se,{labels:se,onChange:fe,disabled:ee})})]}),me?e("p",{class:"form-error",role:"alert",children:me}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:ee,onClick:he,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:ee,children:ee?f?"Saving\u2026":"Creating\u2026":f?"Save changes":"Create LightVM"})]})]})})}var Ja=[{id:"kernels",label:"Kernels"},{id:"initramfs",label:"Initramfs"},{id:"instances",label:"LightVMs"}];function Xa(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function bn(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function at(t){return[...t].sort((n,a)=>n.name.localeCompare(a.name,void 0,{sensitivity:"base"}))}function Za(t){return t==="kernel"?"kernels":"initramfs"}function _i(t){return t==="kernel"?"/api/lightvm/kernels":"/api/lightvm/initramfs"}function Ya(){let[t,n]=c([]),[a,l]=c([]),[s,o]=c([]),[f,r]=c([]),[b,w]=c("kernels"),[d,C]=c(!0),[h,A]=c(""),[I,m]=c(""),[y,P]=c(""),[L,S]=c(""),[N,p]=c(null),[T,B]=c(null),[K,Z]=c(null),ie=Ne("lightvm_kernel"),x=Ne("lightvm_initramfs"),R=Ne("lightvm"),F=ze(),W=u=>{n(u),window.dispatchEvent(new CustomEvent("rookery:lightvm-kernels-changed",{detail:u}))},Q=u=>{l(u),window.dispatchEvent(new CustomEvent("rookery:lightvm-initramfs-changed",{detail:u}))},g=u=>{o(u),window.dispatchEvent(new CustomEvent("rookery:lightvm-instances-changed",{detail:u}))},oe=async(u=!0)=>{try{let _=await V("/api/lightvm/kernels");W(at(Array.isArray(_)?_:[])),u&&A("")}catch(_){u&&A(_ instanceof Error?_.message:String(_))}},E=async(u=!0)=>{try{let _=await V("/api/lightvm/initramfs");Q(at(Array.isArray(_)?_:[])),u&&m("")}catch(_){u&&m(_ instanceof Error?_.message:String(_))}},z=async(u=!0)=>{try{let _=await V("/api/lightvm/instances");g(at(Array.isArray(_)?_:[])),u&&P("")}catch(_){u&&P(_ instanceof Error?_.message:String(_))}},re=async()=>{try{let u=await V("/api/interfaces");r(Array.isArray(u)?u:[])}catch{r([])}},ue=async()=>{try{let u=await V("/api/status");p(u.lightvm??null)}catch{p(null)}},v=async()=>{C(!0),await Promise.allSettled([oe(),E(),z(),re(),ue()]),C(!1)};G(()=>{v();let u=J=>{J.detail==="lightvm"&&v()},_=()=>te("/lightvm/kernels/new"),$=()=>te("/lightvm/initramfs/new"),j=()=>te("/lightvm/instances/new");return window.addEventListener("rookery:tab-changed",u),window.addEventListener("rookery:add-lightvm-kernel",_),window.addEventListener("rookery:add-lightvm-initramfs",$),window.addEventListener("rookery:add-lightvm-instance",j),()=>{window.removeEventListener("rookery:tab-changed",u),window.removeEventListener("rookery:add-lightvm-kernel",_),window.removeEventListener("rookery:add-lightvm-initramfs",$),window.removeEventListener("rookery:add-lightvm-instance",j)}},[]),G(()=>{let[u,_,$,j]=F.segments;if(u!=="lightvm"){B(null),Z(null);return}if(_&&Ja.some(J=>J.id===_)?w(_):w("kernels"),_==="instances"){if(B(null),$==="new"){Z({vm:null});return}if($&&j==="edit"){let J=s.find(pe=>pe.id===$);if(J){Z({vm:J});return}}Z(null);return}if(Z(null),(_==="kernels"||_==="initramfs")&&$==="new"){B({kind:_==="kernels"?"kernel":"initramfs",artifact:null});return}if($&&j==="edit"&&(_==="kernels"||_==="initramfs")){let pe=(_==="kernels"?t:a).find(Le=>String(Le.id)===$);if(pe){B({kind:_==="kernels"?"kernel":"initramfs",artifact:pe});return}}B(null)},[F.path,t,a,s]),G(()=>{let u=document.getElementById("lightvm-tab-count");u&&(u.textContent=String(s.length))},[s.length]);let M=async(u,_)=>{let $=u==="kernel"?"kernel":"initramfs",j=_.managed?"The uploaded file is deleted from the Rookery host.":"The file on the host is left in place.";if(window.confirm(`Delete ${$} "${_.name}"? -${X}`))try{await V(`${yi(c)}/${_.id}`,{method:"DELETE"}),c==="kernel"?(W(t.filter(Z=>Z.id!==_.id)),A("")):(Y(s.filter(Z=>Z.id!==_.id)),m("")),oe(`Deleted ${B} ${_.name}`)}catch(Z){let fe=`Could not delete ${_.name}: ${Z instanceof Error?Z.message:String(Z)}`;c==="kernel"?A(fe):m(fe)}},C=async c=>{let _=`start:${c.id}`;S(_),P("");try{let B=await V(`/api/lightvm/instances/${c.id}/start`,{method:"POST"});y(a.map(X=>X.id===B.id?B:X)),oe(`Started LightVM ${B.name}`)}catch(B){P(`Could not start ${c.name}: ${B instanceof Error?B.message:String(B)}`),M(!1)}finally{S("")}},U=async c=>{let _=`stop:${c.id}`;S(_),P("");try{let B=await V(`/api/lightvm/instances/${c.id}/stop`,{method:"POST"});y(a.map(X=>X.id===B.id?B:X)),oe(`Stopped LightVM ${B.name}`)}catch(B){P(`Could not stop ${c.name}: ${B instanceof Error?B.message:String(B)}`),M(!1)}finally{S("")}},de=c=>{if(!c.console_enabled){oe("Console access is disabled. Edit the LightVM to enable it.",!0);return}if(c.status!=="running"){oe("Start the LightVM before opening its console.",!0);return}window.open(`/lightvm-console.html?vm=${encodeURIComponent(c.id)}`,"_blank","noopener")},ie=async c=>{if(window.confirm(`Delete LightVM "${c.name}"? +${j}`))try{await V(`${_i(u)}/${_.id}`,{method:"DELETE"}),u==="kernel"?(W(t.filter(J=>J.id!==_.id)),A("")):(Q(a.filter(J=>J.id!==_.id)),m("")),ae(`Deleted ${$} ${_.name}`)}catch(J){let pe=`Could not delete ${_.name}: ${J instanceof Error?J.message:String(J)}`;u==="kernel"?A(pe):m(pe)}},k=async u=>{let _=`start:${u.id}`;S(_),P("");try{let $=await V(`/api/lightvm/instances/${u.id}/start`,{method:"POST"});g(s.map(j=>j.id===$.id?$:j)),ae(`Started LightVM ${$.name}`)}catch($){P(`Could not start ${u.name}: ${$ instanceof Error?$.message:String($)}`),z(!1)}finally{S("")}},O=async u=>{let _=`stop:${u.id}`;S(_),P("");try{let $=await V(`/api/lightvm/instances/${u.id}/stop`,{method:"POST"});g(s.map(j=>j.id===$.id?$:j)),ae(`Stopped LightVM ${$.name}`)}catch($){P(`Could not stop ${u.name}: ${$ instanceof Error?$.message:String($)}`),z(!1)}finally{S("")}},le=u=>{if(!u.console_enabled){ae("Console access is disabled. Edit the LightVM to enable it.",!0);return}if(u.status!=="running"){ae("Start the LightVM before opening its console.",!0);return}window.open(`/lightvm-console.html?vm=${encodeURIComponent(u.id)}`,"_blank","noopener")},se=async u=>{if(window.confirm(`Delete LightVM "${u.name}"? -The definition is removed and its network interfaces are released. No workload is running.`))try{await V(`/api/lightvm/instances/${c.id}`,{method:"DELETE"}),y(a.filter(_=>_.id!==c.id)),se(),P(""),oe(`Deleted LightVM ${c.name}`)}catch(_){P(`Could not delete ${c.name}: ${_ instanceof Error?_.message:String(_)}`)}},pe=t.length===0?u?"Loading kernels\u2026":"No LightVM kernels uploaded":`${t.length} kernel${t.length===1?"":"s"}`,ee=s.length===0?u?"Loading initramfs images\u2026":"No LightVM initramfs uploaded":`${s.length} initramfs image${s.length===1?"":"s"}`,ue=a.length===0?u?"Loading LightVMs\u2026":"No LightVMs defined":`${a.length} LightVM${a.length===1?"":"s"} \xB7 ${a.filter(c=>c.status==="running").length} running`,me=N?.ready!==!1;return e(ge,{children:[N&&!N.ready?e("div",{class:"banner",role:"alert",children:[e("strong",{children:"LightVM runtime unavailable."})," ",N.detail||"Install buildah and virtiofsd before starting a LightVM."]}):null,e("div",{class:"subtabs",role:"tablist","aria-label":"LightVM sections",children:Js.map(c=>e("button",{class:"subtab",type:"button",role:"tab","aria-selected":b===c.id,onClick:()=>Q(`/lightvm/${c.id}`),children:c.label},c.id))}),e("section",{class:"panel","aria-labelledby":"kernels-heading",hidden:b!=="kernels",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"kernels-heading",children:"LightVM kernels"}),e("p",{class:"panel-subtitle",children:pe})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:u,onClick:()=>{v()},children:u?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>Q("/lightvm/kernels/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add kernel"]})]})]}),e("p",{class:"field-hint",children:"Kernel images are uploaded to the host and paired with an initramfs when a LightVM is created. Names are unique; labels are managed by the shared label editor."}),h?e("div",{class:"banner",role:"alert",children:h}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"File"}),e("th",{scope:"col",children:"Size"}),e("th",{scope:"col",children:"Source"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(c=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:c.name}),c.description?e("span",{class:"vm-description",title:c.description,children:c.description}):null,e(De,{entityType:"lightvm_kernel",entityID:c.id,name:c.name,labels:ne.labels[String(c.id)],onSaved:_=>ne.setEntityLabels(String(c.id),_)})]}),e("td",{children:[e("span",{class:"vm-name mono",children:c.file_name||"\u2014"}),e("span",{class:"vm-description mono",title:c.path,children:c.path})]}),e("td",{class:"mono",children:Xs(c.size_bytes)}),e("td",{class:"mono",children:c.managed?"uploaded":"host path"}),e("td",{class:"updated hide-sm",children:hn(c.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>Q(`/lightvm/kernels/${c.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{$("kernel",c)},children:"Delete"})]})})]},c.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2301"}),e("h3",{children:"No LightVM kernels yet"}),e("p",{children:"Upload a kernel image to use as the boot kernel for LightVM instances."}),e("button",{class:"button primary",type:"button",onClick:()=>Q("/lightvm/kernels/new"),children:"Add kernel"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"initramfs-heading",hidden:b!=="initramfs",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"initramfs-heading",children:"LightVM initramfs"}),e("p",{class:"panel-subtitle",children:ee})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:u,onClick:()=>{v()},children:u?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>Q("/lightvm/initramfs/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add initramfs"]})]})]}),e("p",{class:"field-hint",children:"Initramfs images are uploaded to the host and paired with a kernel when a LightVM is created. Names are unique; labels are managed by the shared label editor."}),I?e("div",{class:"banner",role:"alert",children:I}):null,e("div",{class:"table-wrap",children:s.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"File"}),e("th",{scope:"col",children:"Size"}),e("th",{scope:"col",children:"Source"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:s.map(c=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:c.name}),c.description?e("span",{class:"vm-description",title:c.description,children:c.description}):null,e(De,{entityType:"lightvm_initramfs",entityID:c.id,name:c.name,labels:x.labels[String(c.id)],onSaved:_=>x.setEntityLabels(String(c.id),_)})]}),e("td",{children:[e("span",{class:"vm-name mono",children:c.file_name||"\u2014"}),e("span",{class:"vm-description mono",title:c.path,children:c.path})]}),e("td",{class:"mono",children:Xs(c.size_bytes)}),e("td",{class:"mono",children:c.managed?"uploaded":"host path"}),e("td",{class:"updated hide-sm",children:hn(c.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>Q(`/lightvm/initramfs/${c.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{$("initramfs",c)},children:"Delete"})]})})]},c.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"No LightVM initramfs yet"}),e("p",{children:"Upload an initramfs image to pair with a kernel for LightVM instances."}),e("button",{class:"button primary",type:"button",onClick:()=>Q("/lightvm/initramfs/new"),children:"Add initramfs"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"lightvms-heading",hidden:b!=="instances",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"lightvms-heading",children:"LightVMs"}),e("p",{class:"panel-subtitle",children:ue})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:u,onClick:()=>{v()},children:u?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",disabled:t.length===0||s.length===0,title:t.length===0?"Upload a kernel first.":s.length===0?"Upload an initramfs first.":"",onClick:()=>Q("/lightvm/instances/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add LightVM"]})]})]}),e("p",{class:"field-hint",children:"A LightVM combines uploaded kernel and initramfs images with a container image, environment, executable path, and arguments. Definitions are inventory-only until the runtime is added."}),g?e("div",{class:"banner",role:"alert",children:g}):null,e("div",{class:"table-wrap",children:a.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Status"}),e("th",{scope:"col",children:"Resources"}),e("th",{scope:"col",children:"Boot images"}),e("th",{scope:"col",children:"Container image"}),e("th",{scope:"col",children:"Workload"}),e("th",{scope:"col",children:"Interfaces"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:a.map(c=>{let _=Array.isArray(c.executable_args)?c.executable_args:[],B=Array.isArray(c.env)?c.env:[],X=Array.isArray(c.interfaces)?c.interfaces:[],Z=Array.isArray(c.consoles)?c.consoles:[],fe=c.status==="running"||c.status==="starting",Ie=L===`start:${c.id}`,le=L===`stop:${c.id}`;return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:c.name}),c.description?e("span",{class:"vm-description",title:c.description,children:c.description}):null,e(De,{entityType:"lightvm",entityID:c.id,name:c.name,labels:R.labels[c.id],onSaved:be=>R.setEntityLabels(c.id,be)})]}),e("td",{children:[e("span",{class:"status-badge","data-status":c.status,children:c.status||"unknown"}),c.console_socket?e("span",{class:"vm-description mono",title:c.console_socket,children:c.console_socket}):null]}),e("td",{class:"mono",children:[e("span",{children:[c.vcpus," vCPU \xB7 ",c.memory_mb," MB"]}),c.console_enabled?e("span",{class:"vm-description",children:"console enabled"}):null,e("span",{class:"vm-description",children:[Z.length," console",Z.length===1?"":"s"]}),c.cgroup_class?e("span",{class:"vm-description",children:["cgroup ",c.cgroup_class]}):null]}),e("td",{children:[e("span",{class:"vm-name",children:c.kernel_name||"\u2014"}),e("span",{class:"vm-description",children:c.initramfs_name||"\u2014"}),e("span",{class:"vm-description mono",title:c.append,children:c.append?`append: ${c.append}`:"append: none"})]}),e("td",{class:"mono",children:c.image}),e("td",{children:[e("span",{class:"vm-name mono",title:c.executable_path,children:c.executable_path}),e("span",{class:"vm-description mono",title:_.join(" "),children:_.length?_.join(" "):"no arguments"}),e("span",{class:"vm-description",children:[B.length," env var",B.length===1?"":"s"]})]}),e("td",{class:X.length?"mono":"mono muted",title:X.map(be=>be.name).join(", "),children:X.length?X.map(be=>be.name).join(", "):"none"}),e("td",{class:"updated hide-sm",children:hn(c.updated_at)}),e("td",{children:e("div",{class:"actions",children:[fe?e("button",{class:"row-action",type:"button",disabled:le,onClick:()=>{U(c)},children:le?"Stopping\u2026":"Stop"}):e("button",{class:"row-action",type:"button",disabled:Ie||!me,title:me?"":N?.detail||"Install buildah and virtiofsd before starting a LightVM.",onClick:()=>{C(c)},children:Ie?"Starting\u2026":"Start"}),e("button",{class:"row-action",type:"button",disabled:!c.console_enabled||c.status!=="running",title:c.console_enabled?c.status!=="running"?"Start the LightVM before opening its console.":"":"Console access is disabled. Edit the LightVM to enable it.",onClick:()=>de(c),children:"Console"}),e("button",{class:"row-action",type:"button",disabled:fe,title:fe?"Stop the LightVM before editing it.":"",onClick:()=>Q(`/lightvm/instances/${c.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{ie(c)},children:"Delete"})]})})]},c.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A2"}),e("h3",{children:"No LightVMs yet"}),e("p",{children:"Upload a kernel and initramfs, then define a container-backed LightVM."}),e("button",{class:"button primary",type:"button",disabled:t.length===0||s.length===0,title:t.length===0?"Upload a kernel first.":s.length===0?"Upload an initramfs first.":"",onClick:()=>Q("/lightvm/instances/new"),children:"Add LightVM"})]})})]}),T?e(Gs,{kind:T.kind,artifact:T.artifact,onClose:()=>{H(null),Q(`/lightvm/${Zs(T.kind)}`,!0)},onSaved:(c,_)=>{let B=T.kind;if(H(null),Q(`/lightvm/${Zs(B)}`,!0),B==="kernel"){let Z=c;W(st(_?[...t,Z]:t.map(fe=>fe.id===Z.id?Z:fe))),A("")}else{let Z=c;Y(st(_?[...s,Z]:s.map(fe=>fe.id===Z.id?Z:fe))),m("")}let X=B==="kernel"?"kernel":"initramfs";oe(_?`Added ${X} ${c.name}`:`Saved ${X} ${c.name}`)}},`${T.kind}-${T.artifact?T.artifact.id:"new"}`):null,G?e(Ks,{vm:G.vm,kernels:t,initramfs:s,interfaces:f,onClose:()=>{J(null),Q("/lightvm/instances",!0)},onSaved:c=>{let _=G.vm===null;J(null),Q("/lightvm/instances",!0),y(st(_?[...a,c]:a.map(B=>B.id===c.id?c:B))),P(""),se(),oe(_?`Created LightVM ${c.name}`:`Saved LightVM ${c.name}`)}},G.vm?G.vm.id:"new"):null]})}function Ys(t){t&&Fe(e(js,{}),t)}var _i="[A-Za-z0-9][A-Za-z0-9._\\-]*";function Qs({onClose:t,onSaved:n}){let[s,l]=d(""),[a,o]=d(""),[f,r]=ke("media"),[b,w]=d(!1),[u,k]=d(""),h=te(null),A=te(null),I=te(null);z(()=>{h.current?.showModal(),A.current?.focus()},[]);let m=()=>{b||h.current?.close()},g=async P=>{if(P.preventDefault(),!(b||!I.current?.reportValidity())){k(""),w(!0);try{let L=await V("/api/media",{method:"POST",body:JSON.stringify({name:s.trim(),path:a.trim()})});await Ce("media",L.id,L.name,f),n(L)}catch(L){k(L instanceof Error?L.message:String(L))}finally{w(!1)}}};return e("dialog",{ref:h,id:"media-dialog","aria-labelledby":"media-dialog-title",onClose:t,onClick:P=>{P.target===P.currentTarget&&m()},onCancel:P=>{b&&P.preventDefault()},children:e("form",{ref:I,novalidate:!0,onSubmit:P=>{g(P)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"New media"}),e("h2",{id:"media-dialog-title",children:"Add installation media"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:m,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:A,type:"text",required:!0,maxlength:127,pattern:_i,autocomplete:"off",spellcheck:!1,placeholder:"debian-13-netinst",value:s,onInput:P=>l(P.currentTarget.value)}),e("span",{class:"field-hint",children:"A short label for the media."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"ISO path"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"/srv/isos/debian-13.1.0-amd64-netinst.iso",value:a,onInput:P=>o(P.currentTarget.value)}),e("span",{class:"field-hint",children:"Absolute path to an existing read-only file on the host. The file is never modified."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:f,onChange:r,disabled:b})})]}),u?e("p",{class:"form-error",role:"alert",children:u}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:b,onClick:m,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:b,children:b?"Adding\u2026":"Add media"})]})]})})}function wi(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function ki(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function bn(t,n){return n.filter(s=>s.boot_media_id===t)}function ea(){let[t,n]=d([]),[s,l]=d([]),[a,o]=d(!0),[f,r]=d(""),[b,w]=d(!1),u=Ne("media"),k=ze();z(()=>{if(k.segments[0]!=="media"){w(!1);return}w(k.segments[1]==="new")},[k.path]);let h=S=>{n(S),window.dispatchEvent(new CustomEvent("rookery:media-changed",{detail:S}))},A=async(S=!0)=>{try{let N=await V("/api/media");h(Array.isArray(N)?N:[]),S&&r("")}catch(N){S&&r(N instanceof Error?N.message:String(N))}},I=async()=>{try{let S=await V("/api/vms");l(Array.isArray(S)?S:[])}catch{l([])}},m=async()=>{o(!0),await Promise.allSettled([A(),I()]),o(!1)};z(()=>{m();let S=T=>{let H=T.detail;Array.isArray(H)&&n(H)},N=()=>{Q("/media/new")},p=T=>{T.detail==="media"&&m()};return window.addEventListener("rookery:media-data",S),window.addEventListener("rookery:add-media",N),window.addEventListener("rookery:tab-changed",p),()=>{window.removeEventListener("rookery:media-data",S),window.removeEventListener("rookery:add-media",N),window.removeEventListener("rookery:tab-changed",p)}},[]),z(()=>{let S=document.getElementById("media-tab-count");S&&(S.textContent=String(t.length))},[t.length]);let g=async S=>{let N=bn(S.id,s);if(N.length>0){oe(`Clear boot media on ${N.map(T=>T.name).join(", ")} first.`,!0);return}if(window.confirm(`Remove media "${S.name}"? +The definition is removed and its network interfaces are released. No workload is running.`))try{await V(`/api/lightvm/instances/${u.id}`,{method:"DELETE"}),g(s.filter(_=>_.id!==u.id)),re(),P(""),ae(`Deleted LightVM ${u.name}`)}catch(_){P(`Could not delete ${u.name}: ${_ instanceof Error?_.message:String(_)}`)}},fe=t.length===0?d?"Loading kernels\u2026":"No LightVM kernels uploaded":`${t.length} kernel${t.length===1?"":"s"}`,ee=a.length===0?d?"Loading initramfs images\u2026":"No LightVM initramfs uploaded":`${a.length} initramfs image${a.length===1?"":"s"}`,de=s.length===0?d?"Loading LightVMs\u2026":"No LightVMs defined":`${s.length} LightVM${s.length===1?"":"s"} \xB7 ${s.filter(u=>u.status==="running").length} running`,me=N?.ready!==!1;return e(ge,{children:[N&&!N.ready?e("div",{class:"banner",role:"alert",children:[e("strong",{children:"LightVM runtime unavailable."})," ",N.detail||"Install buildah and virtiofsd before starting a LightVM."]}):null,e("div",{class:"subtabs",role:"tablist","aria-label":"LightVM sections",children:Ja.map(u=>e("button",{class:"subtab",type:"button",role:"tab","aria-selected":b===u.id,onClick:()=>te(`/lightvm/${u.id}`),children:u.label},u.id))}),e("section",{class:"panel","aria-labelledby":"kernels-heading",hidden:b!=="kernels",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"kernels-heading",children:"LightVM kernels"}),e("p",{class:"panel-subtitle",children:fe})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:d,onClick:()=>{v()},children:d?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>te("/lightvm/kernels/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add kernel"]})]})]}),e("p",{class:"field-hint",children:"Kernel images are uploaded to the host and paired with an initramfs when a LightVM is created. Names are unique; labels are managed by the shared label editor."}),h?e("div",{class:"banner",role:"alert",children:h}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"File"}),e("th",{scope:"col",children:"Size"}),e("th",{scope:"col",children:"Source"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(u=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:u.name}),u.description?e("span",{class:"vm-description",title:u.description,children:u.description}):null,e(De,{entityType:"lightvm_kernel",entityID:u.id,name:u.name,labels:ie.labels[String(u.id)],onSaved:_=>ie.setEntityLabels(String(u.id),_)})]}),e("td",{children:[e("span",{class:"vm-name mono",children:u.file_name||"\u2014"}),e("span",{class:"vm-description mono",title:u.path,children:u.path})]}),e("td",{class:"mono",children:Xa(u.size_bytes)}),e("td",{class:"mono",children:u.managed?"uploaded":"host path"}),e("td",{class:"updated hide-sm",children:bn(u.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>te(`/lightvm/kernels/${u.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{M("kernel",u)},children:"Delete"})]})})]},u.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2301"}),e("h3",{children:"No LightVM kernels yet"}),e("p",{children:"Upload a kernel image to use as the boot kernel for LightVM instances."}),e("button",{class:"button primary",type:"button",onClick:()=>te("/lightvm/kernels/new"),children:"Add kernel"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"initramfs-heading",hidden:b!=="initramfs",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"initramfs-heading",children:"LightVM initramfs"}),e("p",{class:"panel-subtitle",children:ee})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:d,onClick:()=>{v()},children:d?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>te("/lightvm/initramfs/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add initramfs"]})]})]}),e("p",{class:"field-hint",children:"Initramfs images are uploaded to the host and paired with a kernel when a LightVM is created. Names are unique; labels are managed by the shared label editor."}),I?e("div",{class:"banner",role:"alert",children:I}):null,e("div",{class:"table-wrap",children:a.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"File"}),e("th",{scope:"col",children:"Size"}),e("th",{scope:"col",children:"Source"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:a.map(u=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:u.name}),u.description?e("span",{class:"vm-description",title:u.description,children:u.description}):null,e(De,{entityType:"lightvm_initramfs",entityID:u.id,name:u.name,labels:x.labels[String(u.id)],onSaved:_=>x.setEntityLabels(String(u.id),_)})]}),e("td",{children:[e("span",{class:"vm-name mono",children:u.file_name||"\u2014"}),e("span",{class:"vm-description mono",title:u.path,children:u.path})]}),e("td",{class:"mono",children:Xa(u.size_bytes)}),e("td",{class:"mono",children:u.managed?"uploaded":"host path"}),e("td",{class:"updated hide-sm",children:bn(u.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>te(`/lightvm/initramfs/${u.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{M("initramfs",u)},children:"Delete"})]})})]},u.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"No LightVM initramfs yet"}),e("p",{children:"Upload an initramfs image to pair with a kernel for LightVM instances."}),e("button",{class:"button primary",type:"button",onClick:()=>te("/lightvm/initramfs/new"),children:"Add initramfs"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"lightvms-heading",hidden:b!=="instances",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"lightvms-heading",children:"LightVMs"}),e("p",{class:"panel-subtitle",children:de})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:d,onClick:()=>{v()},children:d?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",disabled:t.length===0||a.length===0,title:t.length===0?"Upload a kernel first.":a.length===0?"Upload an initramfs first.":"",onClick:()=>te("/lightvm/instances/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add LightVM"]})]})]}),e("p",{class:"field-hint",children:"A LightVM combines uploaded kernel and initramfs images with a container image, environment, executable path, and arguments. Definitions are inventory-only until the runtime is added."}),y?e("div",{class:"banner",role:"alert",children:y}):null,e("div",{class:"table-wrap",children:s.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Status"}),e("th",{scope:"col",children:"Resources"}),e("th",{scope:"col",children:"Boot images"}),e("th",{scope:"col",children:"Container image"}),e("th",{scope:"col",children:"Workload"}),e("th",{scope:"col",children:"Interfaces"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:s.map(u=>{let _=Array.isArray(u.executable_args)?u.executable_args:[],$=Array.isArray(u.env)?u.env:[],j=Array.isArray(u.interfaces)?u.interfaces:[],J=Array.isArray(u.consoles)?u.consoles:[],pe=u.status==="running"||u.status==="starting",Le=L===`start:${u.id}`,q=L===`stop:${u.id}`;return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:u.name}),u.description?e("span",{class:"vm-description",title:u.description,children:u.description}):null,e(De,{entityType:"lightvm",entityID:u.id,name:u.name,labels:R.labels[u.id],onSaved:he=>R.setEntityLabels(u.id,he)})]}),e("td",{children:[e("span",{class:"status-badge","data-status":u.status,children:u.status||"unknown"}),u.console_socket?e("span",{class:"vm-description mono",title:u.console_socket,children:u.console_socket}):null]}),e("td",{class:"mono",children:[e("span",{children:[u.vcpus," vCPU \xB7 ",u.memory_mb," MB"]}),u.console_enabled?e("span",{class:"vm-description",children:"console enabled"}):null,e("span",{class:"vm-description",children:[J.length," console",J.length===1?"":"s"]}),u.cgroup_class?e("span",{class:"vm-description",children:["cgroup ",u.cgroup_class]}):null]}),e("td",{children:[e("span",{class:"vm-name",children:u.kernel_name||"\u2014"}),e("span",{class:"vm-description",children:u.initramfs_name||"\u2014"}),e("span",{class:"vm-description mono",title:u.append,children:u.append?`append: ${u.append}`:"append: none"})]}),e("td",{class:"mono",children:u.image}),e("td",{children:[e("span",{class:"vm-name mono",title:u.executable_path,children:u.executable_path}),e("span",{class:"vm-description mono",title:_.join(" "),children:_.length?_.join(" "):"no arguments"}),e("span",{class:"vm-description",children:[$.length," env var",$.length===1?"":"s"]})]}),e("td",{class:j.length?"mono":"mono muted",title:j.map(he=>he.name).join(", "),children:j.length?j.map(he=>he.name).join(", "):"none"}),e("td",{class:"updated hide-sm",children:bn(u.updated_at)}),e("td",{children:e("div",{class:"actions",children:[pe?e("button",{class:"row-action",type:"button",disabled:q,onClick:()=>{O(u)},children:q?"Stopping\u2026":"Stop"}):e("button",{class:"row-action",type:"button",disabled:Le||!me,title:me?"":N?.detail||"Install buildah and virtiofsd before starting a LightVM.",onClick:()=>{k(u)},children:Le?"Starting\u2026":"Start"}),e("button",{class:"row-action",type:"button",disabled:!u.console_enabled||u.status!=="running",title:u.console_enabled?u.status!=="running"?"Start the LightVM before opening its console.":"":"Console access is disabled. Edit the LightVM to enable it.",onClick:()=>le(u),children:"Console"}),e("button",{class:"row-action",type:"button",disabled:pe,title:pe?"Stop the LightVM before editing it.":"",onClick:()=>te(`/lightvm/instances/${u.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{se(u)},children:"Delete"})]})})]},u.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A2"}),e("h3",{children:"No LightVMs yet"}),e("p",{children:"Upload a kernel and initramfs, then define a container-backed LightVM."}),e("button",{class:"button primary",type:"button",disabled:t.length===0||a.length===0,title:t.length===0?"Upload a kernel first.":a.length===0?"Upload an initramfs first.":"",onClick:()=>te("/lightvm/instances/new"),children:"Add LightVM"})]})})]}),T?e(Ka,{kind:T.kind,artifact:T.artifact,onClose:()=>{B(null),te(`/lightvm/${Za(T.kind)}`,!0)},onSaved:(u,_)=>{let $=T.kind;if(B(null),te(`/lightvm/${Za($)}`,!0),$==="kernel"){let J=u;W(at(_?[...t,J]:t.map(pe=>pe.id===J.id?J:pe))),A("")}else{let J=u;Q(at(_?[...a,J]:a.map(pe=>pe.id===J.id?J:pe))),m("")}let j=$==="kernel"?"kernel":"initramfs";ae(_?`Added ${j} ${u.name}`:`Saved ${j} ${u.name}`)}},`${T.kind}-${T.artifact?T.artifact.id:"new"}`):null,K?e(ja,{vm:K.vm,kernels:t,initramfs:a,interfaces:f,onClose:()=>{Z(null),te("/lightvm/instances",!0)},onSaved:u=>{let _=K.vm===null;Z(null),te("/lightvm/instances",!0),g(at(_?[...s,u]:s.map($=>$.id===u.id?u:$))),P(""),re(),ae(_?`Created LightVM ${u.name}`:`Saved LightVM ${u.name}`)}},K.vm?K.vm.id:"new"):null]})}function Qa(t){t&&Fe(e(Ya,{}),t)}var wi="[A-Za-z0-9][A-Za-z0-9._\\-]*";function es({onClose:t,onSaved:n}){let[a,l]=c(""),[s,o]=c(""),[f,r]=ke("media"),[b,w]=c(!1),[d,C]=c(""),h=ne(null),A=ne(null),I=ne(null);G(()=>{h.current?.showModal(),A.current?.focus()},[]);let m=()=>{b||h.current?.close()},y=async P=>{if(P.preventDefault(),!(b||!I.current?.reportValidity())){C(""),w(!0);try{let L=await V("/api/media",{method:"POST",body:JSON.stringify({name:a.trim(),path:s.trim()})});await Ce("media",L.id,L.name,f),n(L)}catch(L){C(L instanceof Error?L.message:String(L))}finally{w(!1)}}};return e("dialog",{ref:h,id:"media-dialog","aria-labelledby":"media-dialog-title",onClose:t,onClick:P=>{P.target===P.currentTarget&&m()},onCancel:P=>{b&&P.preventDefault()},children:e("form",{ref:I,novalidate:!0,onSubmit:P=>{y(P)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"New media"}),e("h2",{id:"media-dialog-title",children:"Add installation media"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:m,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:A,type:"text",required:!0,maxlength:127,pattern:wi,autocomplete:"off",spellcheck:!1,placeholder:"debian-13-netinst",value:a,onInput:P=>l(P.currentTarget.value)}),e("span",{class:"field-hint",children:"A short label for the media."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"ISO path"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"/srv/isos/debian-13.1.0-amd64-netinst.iso",value:s,onInput:P=>o(P.currentTarget.value)}),e("span",{class:"field-hint",children:"Absolute path to an existing read-only file on the host. The file is never modified."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:f,onChange:r,disabled:b})})]}),d?e("p",{class:"form-error",role:"alert",children:d}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:b,onClick:m,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:b,children:b?"Adding\u2026":"Add media"})]})]})})}function ki(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function Ci(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function gn(t,n){return n.filter(a=>a.boot_media_id===t)}function ts(){let[t,n]=c([]),[a,l]=c([]),[s,o]=c(!0),[f,r]=c(""),[b,w]=c(!1),d=Ne("media"),C=ze();G(()=>{if(C.segments[0]!=="media"){w(!1);return}w(C.segments[1]==="new")},[C.path]);let h=S=>{n(S),window.dispatchEvent(new CustomEvent("rookery:media-changed",{detail:S}))},A=async(S=!0)=>{try{let N=await V("/api/media");h(Array.isArray(N)?N:[]),S&&r("")}catch(N){S&&r(N instanceof Error?N.message:String(N))}},I=async()=>{try{let S=await V("/api/vms");l(Array.isArray(S)?S:[])}catch{l([])}},m=async()=>{o(!0),await Promise.allSettled([A(),I()]),o(!1)};G(()=>{m();let S=T=>{let B=T.detail;Array.isArray(B)&&n(B)},N=()=>{te("/media/new")},p=T=>{T.detail==="media"&&m()};return window.addEventListener("rookery:media-data",S),window.addEventListener("rookery:add-media",N),window.addEventListener("rookery:tab-changed",p),()=>{window.removeEventListener("rookery:media-data",S),window.removeEventListener("rookery:add-media",N),window.removeEventListener("rookery:tab-changed",p)}},[]),G(()=>{let S=document.getElementById("media-tab-count");S&&(S.textContent=String(t.length))},[t.length]);let y=async S=>{let N=gn(S.id,a);if(N.length>0){ae(`Clear boot media on ${N.map(T=>T.name).join(", ")} first.`,!0);return}if(window.confirm(`Remove media "${S.name}"? -This removes the inventory entry only. The ISO file is not deleted.`))try{await V(`/api/media/${S.id}`,{method:"DELETE"}),h(t.filter(T=>T.id!==S.id)),r(""),oe(`Removed media ${S.name}`)}catch(T){r(`Could not remove ${S.name}: ${T instanceof Error?T.message:String(T)}`)}},P=t.filter(S=>bn(S.id,s).length>0).length,L=t.length===0?a?"Loading media\u2026":"No installation media registered":`${t.length} media file${t.length===1?"":"s"} \xB7 ${P} in use`;return e(ge,{children:[e("section",{class:"panel","aria-labelledby":"media-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"media-heading",children:"Installation media"}),e("p",{class:"panel-subtitle",children:L})]}),e("div",{class:"actions",children:e("button",{class:"button ghost",type:"button",disabled:a,onClick:()=>{m()},children:a?"Refreshing\u2026":"Refresh"})})]}),f?e("div",{class:"banner",role:"alert",children:f}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Size"}),e("th",{scope:"col",children:"Used as boot media"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(S=>{let N=bn(S.id,s),p=N.length===0?"not used":N.length===1?N[0].name:`${N[0].name} +${N.length-1}`;return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:S.name}),S.path?e("span",{class:"vm-description",title:S.path,children:S.path}):null,e(De,{entityType:"media",entityID:S.id,name:S.name,labels:u.labels[String(S.id)],onSaved:T=>u.setEntityLabels(String(S.id),T)})]}),e("td",{class:"mono",children:wi(S.size_bytes)}),e("td",{class:N.length?"mono":"mono muted",title:N.length?N.map(T=>T.name).join(", "):void 0,children:p}),e("td",{class:"updated hide-sm",children:ki(S.updated_at)}),e("td",{children:e("div",{class:"actions",children:e("button",{class:"row-action danger",type:"button",disabled:N.length>0,title:N.length>0?`Clear boot media on ${N.map(T=>T.name).join(", ")} first.`:"",onClick:()=>{g(S)},children:"Remove"})})})]},S.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CE"}),e("h3",{children:"No installation media yet"}),e("p",{children:"Register a read-only ISO file from the host, then choose it as a VM's boot media."}),e("button",{class:"button primary",type:"button",onClick:()=>Q("/media/new"),children:"Add media"})]})})]}),b?e(Qs,{onClose:()=>{w(!1),Q("/media",!0)},onSaved:S=>{w(!1),Q("/media",!0),r(""),oe(`Added media ${S.name}`),A()}}):null]})}function ta(t){t&&Fe(e(ea,{}),t)}function Ye(t){return(t??0).toLocaleString()}function gn(t){let n=String(t.action||"").toUpperCase(),s=String(t.protocol||"any").toLowerCase(),l=`${n} ${s} ${t.source||"0.0.0.0/0"} -> ${t.destination||"0.0.0.0/0"}`;if(s==="tcp"||s==="udp"){let a=Number(t.source_port_start)||0,o=Number(t.source_port_end)||0,f=Number(t.destination_port_start)||0,r=Number(t.destination_port_end)||0;(a||o)&&(l+=` sports ${a||"*"}-${o||a||"*"}`),(f||r)&&(l+=` dports ${f||"*"}-${r||f||"*"}`)}return t.description&&(l+=` \u2014 ${t.description}`),l}function Ft(t,n="ACL"){let s=[`${n}: ${t.name||"(unnamed)"}`,`Title: ${t.title||"\u2014"}`,`Description: ${t.description||"\u2014"}`,"Rules:"],l=Array.isArray(t.rules)?t.rules:[];return l.length===0?s.push(" (no rules)"):l.forEach((a,o)=>{s.push(` ${o+1}. ${gn(a)}`)}),s.push("Default action: DENY (implicit)"),s.join(` -`)}function na(t,n){let s=n.toLowerCase();return t.find(l=>l.name.toLowerCase()===s)}function Bt(t,n){return n.filter(s=>[...s.ingress_acls||(s.ingress_acl_name?[s.ingress_acl_name]:[]),...s.egress_acls||(s.egress_acl_name?[s.egress_acl_name]:[])].some(a=>a.toLowerCase()===t.name.toLowerCase()))}function vn(){return{action:"permit",protocol:"any",source:"0.0.0.0/0",destination:"0.0.0.0/0"}}var Ci="[A-Za-z0-9][A-Za-z0-9._\\-]*";function sa(t){return t.map(n=>{let s=n.protocol==="tcp"||n.protocol==="udp";return{action:n.action,description:n.description?.trim()||"",protocol:n.protocol,source:n.source.trim(),destination:n.destination.trim(),source_port_start:s&&Number(n.source_port_start)||0,source_port_end:s&&Number(n.source_port_end)||0,destination_port_start:s&&Number(n.destination_port_start)||0,destination_port_end:s&&Number(n.destination_port_end)||0}})}function aa({acl:t,vppReady:n,onClose:s,onSaved:l}){let a=t!==null,[o,f]=d(t?.name??""),[r,b]=d(t?.title??""),[w,u]=d(t?.description??""),[k,h]=ke("acl",t?.id),[A,I]=d(()=>t?.rules?.length?t.rules.map(R=>({...R})):[vn()]),[m,g]=d(!1),[P,L]=d(""),S=te(null),N=te(null),p=te(null);z(()=>{S.current?.showModal(),N.current?.focus()},[]);let T=()=>{m||S.current?.close()},H=(x,R,F)=>{I(W=>W.map((Y,y)=>y===x?{...Y,[R]:F}:Y))},G=x=>{I(R=>R.filter((F,W)=>W!==x))},J=async x=>{if(x.preventDefault(),!(m||!n||!p.current?.reportValidity())){if(A.length===0){L("Add at least one rule.");return}L(""),g(!0);try{let R={name:o.trim(),title:r.trim(),description:w.trim(),rules:sa(A)},F=await V(a?`/api/acls/${t?.id}`:"/api/acls",{method:a?"PUT":"POST",body:JSON.stringify(R)});await Ce("acl",F.id,F.name,k),l(F)}catch(R){L(R instanceof Error?R.message:String(R))}finally{g(!1)}}},ne=Ft({name:o.trim(),title:r.trim(),description:w.trim(),rules:sa(A)});return e("dialog",{ref:S,id:"acl-dialog","aria-labelledby":"acl-dialog-title",onClose:s,onClick:x=>{x.target===x.currentTarget&&T()},onCancel:x=>{m&&x.preventDefault()},children:e("form",{ref:p,novalidate:!0,onSubmit:x=>{J(x)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:a?"Edit ACL":"New ACL"}),e("h2",{id:"acl-dialog-title",children:a?`ACL \xB7 ${t?.name}`:"Define ACL"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:T,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:N,type:"text",required:!0,maxlength:64,pattern:Ci,autocomplete:"off",spellcheck:!1,placeholder:"web-ingress",value:o,onInput:x=>f(x.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Title"}),e("input",{type:"text",maxlength:128,autocomplete:"off",placeholder:"Web server ingress",value:r,onInput:x=>b(x.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Description ",e("em",{children:"optional"})]}),e("textarea",{rows:2,maxlength:2e3,placeholder:"What this ACL protects",value:w,onInput:x=>u(x.currentTarget.value)})]}),e("div",{class:"field field-wide",children:e(Se,{labels:k,onChange:h,disabled:m})})]}),e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:"Ordered rules"}),e("span",{class:"field-hint",children:"First match wins. Traffic that matches no rule is denied."})]}),e("button",{class:"button ghost",type:"button",onClick:()=>I(x=>[...x,vn()]),children:"Add rule"})]}),e("div",{class:"acl-rule-rows",children:A.map((x,R)=>{let F=x.protocol==="tcp"||x.protocol==="udp";return e("div",{class:"acl-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"acl-rule-label",children:["Rule ",R+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>G(R),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field acl-field-acl-rule-action",children:[e("span",{class:"field-label",children:"Action"}),e("select",{value:x.action,onChange:W=>H(R,"action",W.currentTarget.value),children:[e("option",{value:"permit",children:"Permit"}),e("option",{value:"deny",children:"Deny"})]})]}),e("label",{class:"field acl-field-acl-rule-protocol",children:[e("span",{class:"field-label",children:"Protocol"}),e("select",{value:x.protocol,onChange:W=>H(R,"protocol",W.currentTarget.value),children:[e("option",{value:"any",children:"Any"}),e("option",{value:"tcp",children:"TCP"}),e("option",{value:"udp",children:"UDP"}),e("option",{value:"icmp",children:"ICMP"}),e("option",{value:"icmpv6",children:"ICMPv6"})]})]}),e("label",{class:"field acl-field-acl-rule-source",children:[e("span",{class:"field-label",children:"Source CIDR"}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"0.0.0.0/0",value:x.source,onInput:W=>H(R,"source",W.currentTarget.value)})]}),e("label",{class:"field acl-field-acl-rule-destination",children:[e("span",{class:"field-label",children:"Destination CIDR"}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"0.0.0.0/0",value:x.destination,onInput:W=>H(R,"destination",W.currentTarget.value)})]}),e("label",{class:"field acl-field-acl-rule-description",children:[e("span",{class:"field-label",children:"Description (optional)"}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"Why this rule exists",value:x.description||"",onInput:W=>H(R,"description",W.currentTarget.value)})]}),e("label",{class:"field acl-field-acl-rule-source-start",children:[e("span",{class:"field-label",children:"Source ports"}),e("div",{class:"acl-port-pair",children:[e("label",{class:"acl-port-input",children:[e("span",{children:"Start"}),e("input",{type:"number",min:1,max:65535,placeholder:"any",disabled:!F,value:x.source_port_start||"",onInput:W=>H(R,"source_port_start",Number(W.currentTarget.value))})]}),e("label",{class:"acl-port-input",children:[e("span",{children:"End"}),e("input",{type:"number",min:1,max:65535,placeholder:"any",disabled:!F,value:x.source_port_end||"",onInput:W=>H(R,"source_port_end",Number(W.currentTarget.value))})]})]})]}),e("label",{class:"field acl-field-acl-rule-destination-start",children:[e("span",{class:"field-label",children:"Destination ports"}),e("div",{class:"acl-port-pair",children:[e("label",{class:"acl-port-input",children:[e("span",{children:"Start"}),e("input",{type:"number",min:1,max:65535,placeholder:"any",disabled:!F,value:x.destination_port_start||"",onInput:W=>H(R,"destination_port_start",Number(W.currentTarget.value))})]}),e("label",{class:"acl-port-input",children:[e("span",{children:"End"}),e("input",{type:"number",min:1,max:65535,placeholder:"any",disabled:!F,value:x.destination_port_end||"",onInput:W=>H(R,"destination_port_end",Number(W.currentTarget.value))})]})]})]})]})]},R)})})]})}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Full ACL preview"}),e("pre",{class:"acl-preview",children:ne})]}),P?e("p",{class:"form-error",role:"alert",children:P}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:m,onClick:T,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:m||!n,title:n?"":"VPP must be ready to save ACLs.",children:m?"Saving\u2026":"Save ACL"})]})]})})}function oa({iface:t,vms:n,onClose:s,onSaved:l}){let[a,o]=d(t.attached_vm_id||""),[f,r]=ke("interface",t.id),[b,w]=d(!1),[u,k]=d(""),h=te(null);z(()=>{h.current?.showModal()},[]);let A=()=>{b||h.current?.close()},I=async m=>{if(m.preventDefault(),!b){k(""),w(!0);try{let g=await V(`/api/interfaces/${t.id}`,{method:"PUT",body:JSON.stringify({attached_vm_id:a||null})});await Ce("interface",g.id,g.name,f),l(g)}catch(g){k(g instanceof Error?g.message:String(g))}finally{w(!1)}}};return e("dialog",{ref:h,id:"attach-dialog","aria-labelledby":"attach-dialog-title",onClose:s,onClick:m=>{m.target===m.currentTarget&&A()},onCancel:m=>{b&&m.preventDefault()},children:e("form",{novalidate:!0,onSubmit:m=>{I(m)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"VM attachment"}),e("h2",{id:"attach-dialog-title",children:"Attach interface"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:A,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Interface"}),e("input",{type:"text",readonly:!0,value:t.name})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Attach to VM"}),e("select",{value:a,onChange:m=>o(m.currentTarget.value),children:[e("option",{value:"",children:"Leave unattached"}),n.map(m=>{let g=m.status==="running"||m.status==="starting";return e("option",{value:m.id,children:g?`${m.name} (running \u2014 stop first)`:m.name},m.id)})]}),e("span",{class:"field-hint",children:"Interfaces take effect when the VM is started."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:f,onChange:r,disabled:b})})]}),u?e("p",{class:"form-error",role:"alert",children:u}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:b,onClick:A,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:b,children:b?"Saving\u2026":"Save attachment"})]})]})})}var Si="[A-Za-z0-9][A-Za-z0-9._\\-]*";function ia({networks:t,onClose:n,onSaved:s}){let[l,a]=d(t[0]?String(t[0].id):""),[o,f]=d(""),[r,b]=d(""),[w,u]=ke("host_tap"),[k,h]=d(!1),[A,I]=d(""),m=te(null),g=te(null),P=te(null);z(()=>{m.current?.showModal(),g.current?.focus()},[]);let L=()=>{k||m.current?.close()},S=t.find(p=>String(p.id)===l),N=async p=>{if(p.preventDefault(),!(k||!P.current?.reportValidity())){I(""),h(!0);try{let T=await V("/api/host/taps",{method:"POST",body:JSON.stringify({network_id:Number(l),name:o.trim(),ipv4_address:r.trim()})});await Ce("host_tap",T.id,T.name,w),s(T)}catch(T){I(T instanceof Error?T.message:String(T))}finally{h(!1)}}};return e("dialog",{ref:m,id:"host-tap-dialog","aria-labelledby":"host-tap-dialog-title",onClose:n,onClick:p=>{p.target===p.currentTarget&&L()},onCancel:p=>{k&&p.preventDefault()},children:e("form",{ref:P,novalidate:!0,onSubmit:p=>{N(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Network to host bridge"}),e("h2",{id:"host-tap-dialog-title",children:"Add host tap"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:L,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Rookery Network"}),e("select",{ref:g,required:!0,value:l,onChange:p=>a(p.currentTarget.value),children:t.map(p=>e("option",{value:p.id,children:[p.name,p.ipv4_cidr?` \xB7 ${p.ipv4_cidr}`:""]},p.id))}),e("span",{class:"field-hint",children:"The tap is bridged into this network, so the host sees its machines at layer 2."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Linux interface name"}),e("input",{type:"text",maxlength:15,pattern:Si,autocomplete:"off",spellcheck:!1,placeholder:S?`hostnet${S.id}`:"hostnet1",value:o,onInput:p=>f(p.currentTarget.value)}),e("span",{class:"field-hint",children:"Optional; defaults to hostnet. Maximum 15 characters."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Host IPv4 address"}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"10.0.0.50/16",value:r,onInput:p=>b(p.currentTarget.value)}),e("span",{class:"field-hint",children:"Optional CIDR inside the network, outside the first five reserved addresses. Leave empty to use DHCP or configure the interface manually."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:w,onChange:u,disabled:k})})]}),A?e("p",{class:"form-error",role:"alert",children:A}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:k,onClick:L,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:k||!l,children:k?"Creating\u2026":"Create host tap"})]})]})})}function ra({label:t,hint:n,acls:s,selected:l,onChange:a}){let[o,f]=d(""),r=s.filter(u=>!l.includes(u.name)),b=()=>{o&&(a([...l,o]),f(""))},w=(u,k)=>{let h=u+k;if(h<0||h>=l.length)return;let A=l.slice();[A[u],A[h]]=[A[h],A[u]],a(A)};return e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:t}),e("div",{class:"acl-list-editor",children:[e("div",{class:"acl-list-picker",children:[e("select",{value:o,"aria-label":`Select ${t} ACL`,onChange:u=>f(u.currentTarget.value),children:[e("option",{value:"",children:"Select ACL\u2026"}),r.map(u=>e("option",{value:u.name,children:u.title?`${u.name} \u2014 ${u.title}`:u.name},u.id))]}),e("button",{class:"button ghost",type:"button",disabled:!o,onClick:b,children:"Add"})]}),e("div",{class:"acl-list-selected","aria-label":`Selected ${t} ACL order`,children:l.length===0?e("span",{class:"muted",children:"None"}):l.map((u,k)=>e("div",{class:"acl-order-row",children:[e("span",{class:"mono",children:[k+1,". ",u]}),e("button",{class:"button ghost",type:"button","aria-label":`Move ${u} up`,disabled:k===0,onClick:()=>w(k,-1),children:"\u2191"}),e("button",{class:"button ghost",type:"button","aria-label":`Move ${u} down`,disabled:k===l.length-1,onClick:()=>w(k,1),children:"\u2193"}),e("button",{class:"button ghost danger",type:"button","aria-label":`Remove ${u}`,onClick:()=>a(l.filter(h=>h!==u)),children:"\xD7"})]},u))})]}),e("span",{class:"field-hint",children:n})]})}function la({iface:t,acls:n,vppReady:s,onClose:l,onSaved:a}){let o=t.ingress_acls?.length?t.ingress_acls:t.ingress_acl_name?[t.ingress_acl_name]:[],f=t.egress_acls?.length?t.egress_acls:t.egress_acl_name?[t.egress_acl_name]:[],[r,b]=d(o),[w,u]=d(f),[k,h]=d(!1),[A,I]=d(""),m=te(null);z(()=>{m.current?.showModal()},[]);let g=()=>{k||m.current?.close()},P=async p=>{if(p.preventDefault(),!(k||!s)){I(""),h(!0);try{let T=await V(`/api/interfaces/${t.id}/acls`,{method:"PUT",body:JSON.stringify({ingress_acls:r,egress_acls:w})});a(T)}catch(T){I(T instanceof Error?T.message:String(T))}finally{h(!1)}}},L=[],S=(p,T)=>{if(T.length===0){L.push(`${p}: none`);return}T.forEach((H,G)=>{let J=na(n,H);L.push(J?Ft(J,`${p} #${G+1}`):`${p} #${G+1}: ${H} (missing)`)})};S("From interface ACL",r),S("To interface ACL",w);let N=L.join(` +This removes the inventory entry only. The ISO file is not deleted.`))try{await V(`/api/media/${S.id}`,{method:"DELETE"}),h(t.filter(T=>T.id!==S.id)),r(""),ae(`Removed media ${S.name}`)}catch(T){r(`Could not remove ${S.name}: ${T instanceof Error?T.message:String(T)}`)}},P=t.filter(S=>gn(S.id,a).length>0).length,L=t.length===0?s?"Loading media\u2026":"No installation media registered":`${t.length} media file${t.length===1?"":"s"} \xB7 ${P} in use`;return e(ge,{children:[e("section",{class:"panel","aria-labelledby":"media-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"media-heading",children:"Installation media"}),e("p",{class:"panel-subtitle",children:L})]}),e("div",{class:"actions",children:e("button",{class:"button ghost",type:"button",disabled:s,onClick:()=>{m()},children:s?"Refreshing\u2026":"Refresh"})})]}),f?e("div",{class:"banner",role:"alert",children:f}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Size"}),e("th",{scope:"col",children:"Used as boot media"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(S=>{let N=gn(S.id,a),p=N.length===0?"not used":N.length===1?N[0].name:`${N[0].name} +${N.length-1}`;return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:S.name}),S.path?e("span",{class:"vm-description",title:S.path,children:S.path}):null,e(De,{entityType:"media",entityID:S.id,name:S.name,labels:d.labels[String(S.id)],onSaved:T=>d.setEntityLabels(String(S.id),T)})]}),e("td",{class:"mono",children:ki(S.size_bytes)}),e("td",{class:N.length?"mono":"mono muted",title:N.length?N.map(T=>T.name).join(", "):void 0,children:p}),e("td",{class:"updated hide-sm",children:Ci(S.updated_at)}),e("td",{children:e("div",{class:"actions",children:e("button",{class:"row-action danger",type:"button",disabled:N.length>0,title:N.length>0?`Clear boot media on ${N.map(T=>T.name).join(", ")} first.`:"",onClick:()=>{y(S)},children:"Remove"})})})]},S.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CE"}),e("h3",{children:"No installation media yet"}),e("p",{children:"Register a read-only ISO file from the host, then choose it as a VM's boot media."}),e("button",{class:"button primary",type:"button",onClick:()=>te("/media/new"),children:"Add media"})]})})]}),b?e(es,{onClose:()=>{w(!1),te("/media",!0)},onSaved:S=>{w(!1),te("/media",!0),r(""),ae(`Added media ${S.name}`),A()}}):null]})}function ns(t){t&&Fe(e(ts,{}),t)}function Ye(t){return(t??0).toLocaleString()}function vn(t){let n=String(t.action||"").toUpperCase(),a=String(t.protocol||"any").toLowerCase(),l=`${n} ${a} ${t.source||"0.0.0.0/0"} -> ${t.destination||"0.0.0.0/0"}`;if(a==="tcp"||a==="udp"){let s=Number(t.source_port_start)||0,o=Number(t.source_port_end)||0,f=Number(t.destination_port_start)||0,r=Number(t.destination_port_end)||0;(s||o)&&(l+=` sports ${s||"*"}-${o||s||"*"}`),(f||r)&&(l+=` dports ${f||"*"}-${r||f||"*"}`)}return t.description&&(l+=` \u2014 ${t.description}`),l}function Ft(t,n="ACL"){let a=[`${n}: ${t.name||"(unnamed)"}`,`Title: ${t.title||"\u2014"}`,`Description: ${t.description||"\u2014"}`,"Rules:"],l=Array.isArray(t.rules)?t.rules:[];return l.length===0?a.push(" (no rules)"):l.forEach((s,o)=>{a.push(` ${o+1}. ${vn(s)}`)}),a.push("Default action: DENY (implicit)"),a.join(` +`)}function as(t,n){let a=n.toLowerCase();return t.find(l=>l.name.toLowerCase()===a)}function Bt(t,n){return n.filter(a=>[...a.ingress_acls||(a.ingress_acl_name?[a.ingress_acl_name]:[]),...a.egress_acls||(a.egress_acl_name?[a.egress_acl_name]:[])].some(s=>s.toLowerCase()===t.name.toLowerCase()))}function yn(){return{action:"permit",protocol:"any",source:"0.0.0.0/0",destination:"0.0.0.0/0"}}var Si="[A-Za-z0-9][A-Za-z0-9._\\-]*";function ss(t){return t.map(n=>{let a=n.protocol==="tcp"||n.protocol==="udp";return{action:n.action,description:n.description?.trim()||"",protocol:n.protocol,source:n.source.trim(),destination:n.destination.trim(),source_port_start:a&&Number(n.source_port_start)||0,source_port_end:a&&Number(n.source_port_end)||0,destination_port_start:a&&Number(n.destination_port_start)||0,destination_port_end:a&&Number(n.destination_port_end)||0}})}function os({acl:t,vppReady:n,onClose:a,onSaved:l}){let s=t!==null,[o,f]=c(t?.name??""),[r,b]=c(t?.title??""),[w,d]=c(t?.description??""),[C,h]=ke("acl",t?.id),[A,I]=c(()=>t?.rules?.length?t.rules.map(R=>({...R})):[yn()]),[m,y]=c(!1),[P,L]=c(""),S=ne(null),N=ne(null),p=ne(null);G(()=>{S.current?.showModal(),N.current?.focus()},[]);let T=()=>{m||S.current?.close()},B=(x,R,F)=>{I(W=>W.map((Q,g)=>g===x?{...Q,[R]:F}:Q))},K=x=>{I(R=>R.filter((F,W)=>W!==x))},Z=async x=>{if(x.preventDefault(),!(m||!n||!p.current?.reportValidity())){if(A.length===0){L("Add at least one rule.");return}L(""),y(!0);try{let R={name:o.trim(),title:r.trim(),description:w.trim(),rules:ss(A)},F=await V(s?`/api/acls/${t?.id}`:"/api/acls",{method:s?"PUT":"POST",body:JSON.stringify(R)});await Ce("acl",F.id,F.name,C),l(F)}catch(R){L(R instanceof Error?R.message:String(R))}finally{y(!1)}}},ie=Ft({name:o.trim(),title:r.trim(),description:w.trim(),rules:ss(A)});return e("dialog",{ref:S,id:"acl-dialog","aria-labelledby":"acl-dialog-title",onClose:a,onClick:x=>{x.target===x.currentTarget&&T()},onCancel:x=>{m&&x.preventDefault()},children:e("form",{ref:p,novalidate:!0,onSubmit:x=>{Z(x)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:s?"Edit ACL":"New ACL"}),e("h2",{id:"acl-dialog-title",children:s?`ACL \xB7 ${t?.name}`:"Define ACL"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:T,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:N,type:"text",required:!0,maxlength:64,pattern:Si,autocomplete:"off",spellcheck:!1,placeholder:"web-ingress",value:o,onInput:x=>f(x.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Title"}),e("input",{type:"text",maxlength:128,autocomplete:"off",placeholder:"Web server ingress",value:r,onInput:x=>b(x.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Description ",e("em",{children:"optional"})]}),e("textarea",{rows:2,maxlength:2e3,placeholder:"What this ACL protects",value:w,onInput:x=>d(x.currentTarget.value)})]}),e("div",{class:"field field-wide",children:e(Se,{labels:C,onChange:h,disabled:m})})]}),e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:"Ordered rules"}),e("span",{class:"field-hint",children:"First match wins. Traffic that matches no rule is denied."})]}),e("button",{class:"button ghost",type:"button",onClick:()=>I(x=>[...x,yn()]),children:"Add rule"})]}),e("div",{class:"acl-rule-rows",children:A.map((x,R)=>{let F=x.protocol==="tcp"||x.protocol==="udp";return e("div",{class:"acl-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"acl-rule-label",children:["Rule ",R+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>K(R),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field acl-field-acl-rule-action",children:[e("span",{class:"field-label",children:"Action"}),e("select",{value:x.action,onChange:W=>B(R,"action",W.currentTarget.value),children:[e("option",{value:"permit",children:"Permit"}),e("option",{value:"deny",children:"Deny"})]})]}),e("label",{class:"field acl-field-acl-rule-protocol",children:[e("span",{class:"field-label",children:"Protocol"}),e("select",{value:x.protocol,onChange:W=>B(R,"protocol",W.currentTarget.value),children:[e("option",{value:"any",children:"Any"}),e("option",{value:"tcp",children:"TCP"}),e("option",{value:"udp",children:"UDP"}),e("option",{value:"icmp",children:"ICMP"}),e("option",{value:"icmpv6",children:"ICMPv6"})]})]}),e("label",{class:"field acl-field-acl-rule-source",children:[e("span",{class:"field-label",children:"Source CIDR"}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"0.0.0.0/0",value:x.source,onInput:W=>B(R,"source",W.currentTarget.value)})]}),e("label",{class:"field acl-field-acl-rule-destination",children:[e("span",{class:"field-label",children:"Destination CIDR"}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"0.0.0.0/0",value:x.destination,onInput:W=>B(R,"destination",W.currentTarget.value)})]}),e("label",{class:"field acl-field-acl-rule-description",children:[e("span",{class:"field-label",children:"Description (optional)"}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"Why this rule exists",value:x.description||"",onInput:W=>B(R,"description",W.currentTarget.value)})]}),e("label",{class:"field acl-field-acl-rule-source-start",children:[e("span",{class:"field-label",children:"Source ports"}),e("div",{class:"acl-port-pair",children:[e("label",{class:"acl-port-input",children:[e("span",{children:"Start"}),e("input",{type:"number",min:1,max:65535,placeholder:"any",disabled:!F,value:x.source_port_start||"",onInput:W=>B(R,"source_port_start",Number(W.currentTarget.value))})]}),e("label",{class:"acl-port-input",children:[e("span",{children:"End"}),e("input",{type:"number",min:1,max:65535,placeholder:"any",disabled:!F,value:x.source_port_end||"",onInput:W=>B(R,"source_port_end",Number(W.currentTarget.value))})]})]})]}),e("label",{class:"field acl-field-acl-rule-destination-start",children:[e("span",{class:"field-label",children:"Destination ports"}),e("div",{class:"acl-port-pair",children:[e("label",{class:"acl-port-input",children:[e("span",{children:"Start"}),e("input",{type:"number",min:1,max:65535,placeholder:"any",disabled:!F,value:x.destination_port_start||"",onInput:W=>B(R,"destination_port_start",Number(W.currentTarget.value))})]}),e("label",{class:"acl-port-input",children:[e("span",{children:"End"}),e("input",{type:"number",min:1,max:65535,placeholder:"any",disabled:!F,value:x.destination_port_end||"",onInput:W=>B(R,"destination_port_end",Number(W.currentTarget.value))})]})]})]})]})]},R)})})]})}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Full ACL preview"}),e("pre",{class:"acl-preview",children:ie})]}),P?e("p",{class:"form-error",role:"alert",children:P}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:m,onClick:T,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:m||!n,title:n?"":"VPP must be ready to save ACLs.",children:m?"Saving\u2026":"Save ACL"})]})]})})}function is({iface:t,vms:n,onClose:a,onSaved:l}){let[s,o]=c(t.attached_vm_id||""),[f,r]=ke("interface",t.id),[b,w]=c(!1),[d,C]=c(""),h=ne(null);G(()=>{h.current?.showModal()},[]);let A=()=>{b||h.current?.close()},I=async m=>{if(m.preventDefault(),!b){C(""),w(!0);try{let y=await V(`/api/interfaces/${t.id}`,{method:"PUT",body:JSON.stringify({attached_vm_id:s||null})});await Ce("interface",y.id,y.name,f),l(y)}catch(y){C(y instanceof Error?y.message:String(y))}finally{w(!1)}}};return e("dialog",{ref:h,id:"attach-dialog","aria-labelledby":"attach-dialog-title",onClose:a,onClick:m=>{m.target===m.currentTarget&&A()},onCancel:m=>{b&&m.preventDefault()},children:e("form",{novalidate:!0,onSubmit:m=>{I(m)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"VM attachment"}),e("h2",{id:"attach-dialog-title",children:"Attach interface"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:A,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Interface"}),e("input",{type:"text",readonly:!0,value:t.name})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Attach to VM"}),e("select",{value:s,onChange:m=>o(m.currentTarget.value),children:[e("option",{value:"",children:"Leave unattached"}),n.map(m=>{let y=m.status==="running"||m.status==="starting";return e("option",{value:m.id,children:y?`${m.name} (running \u2014 stop first)`:m.name},m.id)})]}),e("span",{class:"field-hint",children:"Interfaces take effect when the VM is started."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:f,onChange:r,disabled:b})})]}),d?e("p",{class:"form-error",role:"alert",children:d}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:b,onClick:A,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:b,children:b?"Saving\u2026":"Save attachment"})]})]})})}var Ti="[A-Za-z0-9][A-Za-z0-9._\\-]*";function rs({networks:t,onClose:n,onSaved:a}){let[l,s]=c(t[0]?String(t[0].id):""),[o,f]=c(""),[r,b]=c(""),[w,d]=ke("host_tap"),[C,h]=c(!1),[A,I]=c(""),m=ne(null),y=ne(null),P=ne(null);G(()=>{m.current?.showModal(),y.current?.focus()},[]);let L=()=>{C||m.current?.close()},S=t.find(p=>String(p.id)===l),N=async p=>{if(p.preventDefault(),!(C||!P.current?.reportValidity())){I(""),h(!0);try{let T=await V("/api/host/taps",{method:"POST",body:JSON.stringify({network_id:Number(l),name:o.trim(),ipv4_address:r.trim()})});await Ce("host_tap",T.id,T.name,w),a(T)}catch(T){I(T instanceof Error?T.message:String(T))}finally{h(!1)}}};return e("dialog",{ref:m,id:"host-tap-dialog","aria-labelledby":"host-tap-dialog-title",onClose:n,onClick:p=>{p.target===p.currentTarget&&L()},onCancel:p=>{C&&p.preventDefault()},children:e("form",{ref:P,novalidate:!0,onSubmit:p=>{N(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Network to host bridge"}),e("h2",{id:"host-tap-dialog-title",children:"Add host tap"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:L,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Rookery Network"}),e("select",{ref:y,required:!0,value:l,onChange:p=>s(p.currentTarget.value),children:t.map(p=>e("option",{value:p.id,children:[p.name,p.ipv4_cidr?` \xB7 ${p.ipv4_cidr}`:""]},p.id))}),e("span",{class:"field-hint",children:"The tap is bridged into this network, so the host sees its machines at layer 2."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Linux interface name"}),e("input",{type:"text",maxlength:15,pattern:Ti,autocomplete:"off",spellcheck:!1,placeholder:S?`hostnet${S.id}`:"hostnet1",value:o,onInput:p=>f(p.currentTarget.value)}),e("span",{class:"field-hint",children:"Optional; defaults to hostnet. Maximum 15 characters."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Host IPv4 address"}),e("input",{type:"text",autocomplete:"off",spellcheck:!1,placeholder:"10.0.0.50/16",value:r,onInput:p=>b(p.currentTarget.value)}),e("span",{class:"field-hint",children:"Optional CIDR inside the network, outside the first five reserved addresses. Leave empty to use DHCP or configure the interface manually."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:w,onChange:d,disabled:C})})]}),A?e("p",{class:"form-error",role:"alert",children:A}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:C,onClick:L,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:C||!l,children:C?"Creating\u2026":"Create host tap"})]})]})})}function ls({label:t,hint:n,acls:a,selected:l,onChange:s}){let[o,f]=c(""),r=a.filter(d=>!l.includes(d.name)),b=()=>{o&&(s([...l,o]),f(""))},w=(d,C)=>{let h=d+C;if(h<0||h>=l.length)return;let A=l.slice();[A[d],A[h]]=[A[h],A[d]],s(A)};return e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:t}),e("div",{class:"acl-list-editor",children:[e("div",{class:"acl-list-picker",children:[e("select",{value:o,"aria-label":`Select ${t} ACL`,onChange:d=>f(d.currentTarget.value),children:[e("option",{value:"",children:"Select ACL\u2026"}),r.map(d=>e("option",{value:d.name,children:d.title?`${d.name} \u2014 ${d.title}`:d.name},d.id))]}),e("button",{class:"button ghost",type:"button",disabled:!o,onClick:b,children:"Add"})]}),e("div",{class:"acl-list-selected","aria-label":`Selected ${t} ACL order`,children:l.length===0?e("span",{class:"muted",children:"None"}):l.map((d,C)=>e("div",{class:"acl-order-row",children:[e("span",{class:"mono",children:[C+1,". ",d]}),e("button",{class:"button ghost",type:"button","aria-label":`Move ${d} up`,disabled:C===0,onClick:()=>w(C,-1),children:"\u2191"}),e("button",{class:"button ghost",type:"button","aria-label":`Move ${d} down`,disabled:C===l.length-1,onClick:()=>w(C,1),children:"\u2193"}),e("button",{class:"button ghost danger",type:"button","aria-label":`Remove ${d}`,onClick:()=>s(l.filter(h=>h!==d)),children:"\xD7"})]},d))})]}),e("span",{class:"field-hint",children:n})]})}function cs({iface:t,acls:n,vppReady:a,onClose:l,onSaved:s}){let o=t.ingress_acls?.length?t.ingress_acls:t.ingress_acl_name?[t.ingress_acl_name]:[],f=t.egress_acls?.length?t.egress_acls:t.egress_acl_name?[t.egress_acl_name]:[],[r,b]=c(o),[w,d]=c(f),[C,h]=c(!1),[A,I]=c(""),m=ne(null);G(()=>{m.current?.showModal()},[]);let y=()=>{C||m.current?.close()},P=async p=>{if(p.preventDefault(),!(C||!a)){I(""),h(!0);try{let T=await V(`/api/interfaces/${t.id}/acls`,{method:"PUT",body:JSON.stringify({ingress_acls:r,egress_acls:w})});s(T)}catch(T){I(T instanceof Error?T.message:String(T))}finally{h(!1)}}},L=[],S=(p,T)=>{if(T.length===0){L.push(`${p}: none`);return}T.forEach((B,K)=>{let Z=as(n,B);L.push(Z?Ft(Z,`${p} #${K+1}`):`${p} #${K+1}: ${B} (missing)`)})};S("From interface ACL",r),S("To interface ACL",w);let N=L.join(` -`);return e("dialog",{ref:m,id:"interface-acl-dialog","aria-labelledby":"interface-acl-title",onClose:l,onClick:p=>{p.target===p.currentTarget&&g()},onCancel:p=>{k&&p.preventDefault()},children:e("form",{novalidate:!0,onSubmit:p=>{P(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Interface policy"}),e("h2",{id:"interface-acl-title",children:"Assign ACLs"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:g,children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.name," \xB7 ",t.network_name," \xB7 ",t.mac]}),e("div",{class:"form-grid",children:[e(ra,{label:"From interface",hint:"Applied to packets entering the interface, in this order. VPP evaluates the first matching rule across the list.",acls:n,selected:r,onChange:b}),e(ra,{label:"To interface",hint:"Applied to packets leaving the interface, in this order.",acls:n,selected:w,onChange:u}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Full ACL preview"}),e("pre",{class:"acl-preview",children:N})]})]}),A?e("p",{class:"form-error",role:"alert",children:A}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:k,onClick:g,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:k||!s,title:s?"":"VPP must be ready to apply interface ACLs.",children:k?"Applying\u2026":"Apply ACLs"})]})]})})}var Ti="[A-Za-z0-9][A-Za-z0-9._\\-]*";function da({networks:t,vms:n,onClose:s,onSaved:l}){let[a,o]=d(""),[f,r]=d(t[0]?String(t[0].id):""),[b,w]=d(""),[u,k]=ke("interface"),[h,A]=d(!1),[I,m]=d(""),g=te(null),P=te(null),L=te(null);z(()=>{g.current?.showModal(),P.current?.focus()},[]);let S=()=>{h||g.current?.close()},N=async p=>{if(p.preventDefault(),!(h||!L.current?.reportValidity())){m(""),A(!0);try{let T=await V("/api/interfaces",{method:"POST",body:JSON.stringify({name:a.trim(),network_id:Number(f),attached_vm_id:b||null})});await Ce("interface",T.id,T.name,u),l(T)}catch(T){m(T instanceof Error?T.message:String(T))}finally{A(!1)}}};return e("dialog",{ref:g,id:"interface-dialog","aria-labelledby":"interface-dialog-title",onClose:s,onClick:p=>{p.target===p.currentTarget&&S()},onCancel:p=>{h&&p.preventDefault()},children:e("form",{ref:L,novalidate:!0,onSubmit:p=>{N(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"VPP vhost-user"}),e("h2",{id:"interface-dialog-title",children:"Add virtual interface"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:S,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:P,type:"text",required:!0,maxlength:64,pattern:Ti,autocomplete:"off",spellcheck:!1,placeholder:"vm01-eth0",value:a,onInput:p=>o(p.currentTarget.value)}),e("span",{class:"field-hint",children:"Creates a vhost-user socket under the configured VPP directory."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Rookery Network"}),e("select",{required:!0,value:f,onChange:p=>r(p.currentTarget.value),children:t.map(p=>e("option",{value:String(p.id),children:[p.name," \xB7 bd ",p.bridge_domain_id]},p.id))})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Attach to VM ",e("em",{children:"optional"})]}),e("select",{value:b,onChange:p=>w(p.currentTarget.value),children:[e("option",{value:"",children:"Leave unattached"}),n.map(p=>{let T=p.status==="running"||p.status==="starting";return e("option",{value:p.id,children:T?`${p.name} (running \u2014 stop first)`:p.name},p.id)})]}),e("span",{class:"field-hint",children:"Stop the VM before attaching or detaching interfaces."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:u,onChange:k,disabled:h})})]}),I?e("p",{class:"form-error",role:"alert",children:I}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:h,onClick:S,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:h,children:h?"Creating\u2026":"Add interface"})]})]})})}function ca({iface:t,tables:n,onClose:s,onSaved:l}){let[a,o]=d(String(t.ipv4_table_id??0)),[f,r]=d(String(t.ipv6_table_id??0)),[b,w]=d(!1),[u,k]=d(""),h=te(null);z(()=>{h.current?.showModal()},[]);let A=()=>{b||h.current?.close()},I=async g=>{if(g.preventDefault(),!b){k(""),w(!0);try{let P=await V(`/api/interfaces/${t.id}/route-tables`,{method:"PUT",body:JSON.stringify({ipv4_table_id:Number(a),ipv6_table_id:Number(f)})});l(P)}catch(P){k(P instanceof Error?P.message:String(P))}finally{w(!1)}}},m=g=>n.filter(P=>P.ipv6===g&&P.table_id!==0);return e("dialog",{ref:h,id:"interface-route-tables-dialog","aria-labelledby":"interface-route-tables-title",onClose:s,onClick:g=>{g.target===g.currentTarget&&A()},onCancel:g=>{b&&g.preventDefault()},children:e("form",{novalidate:!0,onSubmit:g=>{I(g)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Interface FIB binding"}),e("h2",{id:"interface-route-tables-title",children:["Route tables \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:A,children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.network_name," \xB7 ",t.mac,". Table 0 is the default VRF."]}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv4 FIB"}),e("select",{value:a,onChange:g=>o(g.currentTarget.value),children:[e("option",{value:"0",children:"default (0)"}),m(!1).map(g=>e("option",{value:String(g.table_id),children:[g.name," (",g.table_id,")"]},`v4-${g.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv6 FIB"}),e("select",{value:f,onChange:g=>r(g.currentTarget.value),children:[e("option",{value:"0",children:"default (0)"}),m(!0).map(g=>e("option",{value:String(g.table_id),children:[g.name," (",g.table_id,")"]},`v6-${g.id}`))]})]})]}),u?e("p",{class:"form-error",role:"alert",children:u}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:b,onClick:A,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:b,children:b?"Applying\u2026":"Apply route tables"})]})]})})}var Pi="[A-Za-z0-9][A-Za-z0-9._\\-]*";function ua({network:t,onClose:n,onSaved:s}){let l=!!t,[a,o]=d(t?.name||""),[f,r]=d(t?.ipv4_cidr||"10.0.0.0/16"),[b,w]=d(t?.ipv6_cidr||"fd00:20::/64"),[u,k]=ke("network",t?.id),[h,A]=d(!1),[I,m]=d(""),g=te(null),P=te(null),L=te(null);z(()=>{g.current?.showModal(),P.current?.focus()},[]);let S=()=>{h||g.current?.close()},N=async p=>{if(p.preventDefault(),!(h||!L.current?.reportValidity())){m(""),A(!0);try{let T=await V(l&&t?`/api/networks/${t.id}`:"/api/networks",{method:l?"PUT":"POST",body:JSON.stringify(l?{ipv4_cidr:f.trim(),ipv6_cidr:b.trim()}:{name:a.trim(),ipv4_cidr:f.trim(),ipv6_cidr:b.trim()})});await Ce("network",T.id,T.name,u),s(T)}catch(T){m(T instanceof Error?T.message:String(T))}finally{A(!1)}}};return e("dialog",{ref:g,id:"network-dialog","aria-labelledby":"network-dialog-title",onClose:n,onClick:p=>{p.target===p.currentTarget&&S()},onCancel:p=>{h&&p.preventDefault()},children:e("form",{ref:L,novalidate:!0,onSubmit:p=>{N(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"VPP bridge domain"}),e("h2",{id:"network-dialog-title",children:l?"Edit Rookery Network":"Create Rookery Network"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:S,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:P,type:"text",required:!0,maxlength:64,pattern:Pi,autocomplete:"off",spellcheck:!1,placeholder:"lab-net",value:a,disabled:l,onInput:p=>o(p.currentTarget.value)}),e("span",{class:"field-hint",children:l?"The network name is immutable.":"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv4 network CIDR"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"10.0.0.0/16",value:f,onInput:p=>r(p.currentTarget.value)}),e("span",{class:"field-hint",children:["Address range for this network; .1 is the BVI and .2 the VNF stack.",l?" CIDRs can only be changed while the network has no VNFs, host taps, or BVI.":""]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv6 network CIDR"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"fd00:20::/64",value:b,onInput:p=>w(p.currentTarget.value)}),e("span",{class:"field-hint",children:"IPv6 range reserved for this network."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:u,onChange:k,disabled:h})})]}),I?e("p",{class:"form-error",role:"alert",children:I}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:h,onClick:S,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:h,children:h?"Creating\u2026":"Create network"})]})]})})}function pa({networks:t,onClose:n,onSaved:s}){let[l,a]=d(t[0]?String(t[0].id):""),[o,f]=d(t[1]?String(t[1].id):""),[r,b]=ke("network_pipe"),[w,u]=d(!1),[k,h]=d(""),A=te(null),I=te(null);z(()=>{A.current?.showModal()},[]);let m=()=>{w||A.current?.close()},g=async P=>{if(P.preventDefault(),!(w||!I.current?.reportValidity())){if(!l||!o||l===o){h("Choose two different networks.");return}u(!0),h("");try{let L=await V("/api/network-pipes",{method:"POST",body:JSON.stringify({network_a_id:Number(l),network_b_id:Number(o)})});await Ce("network_pipe",L.id,`pipes for ${L.network_a_name} and ${L.network_b_name}`,r),s(L)}catch(L){h(L instanceof Error?L.message:String(L))}finally{u(!1)}}};return e("dialog",{ref:A,id:"network-pipe-dialog","aria-labelledby":"network-pipe-title",onClose:n,onClick:P=>{P.target===P.currentTarget&&m()},onCancel:P=>{w&&P.preventDefault()},children:e("form",{ref:I,novalidate:!0,onSubmit:P=>{g(P)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Bridge-domain pipe"}),e("h2",{id:"network-pipe-title",children:"Connect two networks"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:m,children:"\xD7"})]}),e("p",{class:"field-hint",children:"Creates a VPP pipe pair and places one end in each bridge domain. This bridges L2 traffic between the two Rookery Networks."}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Network A"}),e("select",{required:!0,value:l,onChange:P=>a(P.currentTarget.value),children:t.map(P=>e("option",{value:String(P.id),children:P.name},P.id))})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Network B"}),e("select",{required:!0,value:o,onChange:P=>f(P.currentTarget.value),children:t.map(P=>e("option",{value:String(P.id),children:P.name},P.id))})]}),e("div",{class:"field field-wide",children:e(Se,{labels:r,onChange:b,disabled:w})})]}),t.length<2?e("p",{class:"field-hint",children:"Create at least two networks before adding a pipe."}):null,k?e("p",{class:"form-error",role:"alert",children:k}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:w,onClick:m,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:w||t.length<2,children:w?"Creating\u2026":"Create pipe"})]})]})})}var Ei={interface:"any",direction:"both",maxMB:10,snapLength:2048};function Ht(t){return t===void 0||!Number.isFinite(t)||t<0?"\u2014":t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KiB`:t<1024*1024*1024?`${(t/(1024*1024)).toFixed(1)} MiB`:`${(t/(1024*1024*1024)).toFixed(2)} GiB`}function yn(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?t:n.toLocaleString()}function Li(t){switch(t){case"rx":return"RX only";case"tx":return"TX only";case"both":case"rx/tx":return"RX + TX";default:return t||"\u2014"}}function fa(t){return`/api/troubleshoot/capture/files/${encodeURIComponent(t)}`}function ma(){let[t,n]=d([]),[s,l]=d(null),[a,o]=d([]),[f,r]=d(Ei),[b,w]=d(!0),[u,k]=d(!1),[h,A]=d(!1),[I,m]=d(""),[g,P]=d(""),L=Math.min(1e6,Math.max(1,Math.ceil(f.maxMB*1024*1024/(f.snapLength+16)))),S=L*(f.snapLength+16),N=f.maxMB>=1&&f.maxMB<=100&&f.snapLength>=64&&f.snapLength<=9e3,p=!!s?.active,T=async()=>{let x=await V("/api/troubleshoot/capture/files"),R=Array.isArray(x)?x:[];o(R)},H=async()=>{w(!0),P("");let[x,R]=await Promise.allSettled([V("/api/troubleshoot/capture/interfaces"),V("/api/troubleshoot/capture")]);if(x.status==="fulfilled"?n(Array.isArray(x.value)?x.value:[]):P(x.reason instanceof Error?x.reason.message:String(x.reason)),R.status==="fulfilled"){let F=R.value;l(F.status??null),o(Array.isArray(F.files)?F.files:[])}else{g||P(R.reason instanceof Error?R.reason.message:String(R.reason));try{await T()}catch{}}w(!1)};z(()=>{H()},[]),z(()=>{if(!p)return;let x=!1,R=window.setInterval(async()=>{try{let F=await V("/api/troubleshoot/capture");if(x)return;let W=Array.isArray(F.files)?F.files:[];l(F.status??null),o(W),P("")}catch(F){x||P(F instanceof Error?F.message:String(F))}},1500);return()=>{x=!0,window.clearInterval(R)}},[p]);let G=async()=>{if(N){k(!0),P("");try{let x=await V("/api/troubleshoot/capture",{method:"POST",body:JSON.stringify({interface:f.interface,direction:f.direction,max_bytes:Math.round(f.maxMB*1024*1024),snap_length:f.snapLength})});l(x),oe("Packet capture started")}catch(x){let R=x instanceof Error?x.message:String(x);P(R),oe(R,!0)}finally{k(!1)}}},J=async x=>{if(!(p&&s?.file===x.name)&&window.confirm(`Delete ${x.name}?`)){m(x.name),P("");try{await V(fa(x.name),{method:"DELETE"}),o(R=>R.filter(F=>F.name!==x.name)),l(R=>R&&R.file===x.name?{...R,file:""}:R),oe("Packet capture deleted")}catch(R){let F=R instanceof Error?R.message:String(R);P(F),oe(F,!0)}finally{m("")}}},ne=async()=>{A(!0),P("");try{let x=await V("/api/troubleshoot/capture/stop",{method:"POST"});l(x);try{await T()}catch{}oe("Packet capture stopped")}catch(x){let R=x instanceof Error?x.message:String(x);P(R),oe(R,!0)}finally{A(!1)}};return e("div",{class:"capture-panel",children:[e("div",{class:"capture-toolbar",children:[e("span",{class:p?"capture-badge active":"capture-badge",children:p?"Capturing":"Idle"}),e("button",{class:"button ghost",type:"button",disabled:b||p||u||h,onClick:()=>{H()},children:"Refresh"})]}),g?e("div",{class:"banner",role:"alert",children:g}):null,e("div",{class:"capture-form",children:[e("label",{children:[e("span",{children:"Interface"}),e("select",{value:f.interface,disabled:p,onChange:x=>r(R=>({...R,interface:x.currentTarget.value})),children:[e("option",{value:"any",children:"Any interface"}),t.map(x=>e("option",{value:x.name,children:[x.name," \xB7 #",x.sw_if_index,x.type?` \xB7 ${x.type}`:"",x.up?"":" \xB7 down"]},x.name))]})]}),e("label",{children:[e("span",{children:"Direction"}),e("select",{value:f.direction,disabled:p,onChange:x=>r(R=>({...R,direction:x.currentTarget.value})),children:[e("option",{value:"both",children:"RX + TX"}),e("option",{value:"rx",children:"RX only"}),e("option",{value:"tx",children:"TX only"})]})]}),e("label",{children:[e("span",{children:"Max size (MB)"}),e("input",{type:"number",min:1,max:100,step:1,value:f.maxMB,disabled:p,onChange:x=>r(R=>({...R,maxMB:Number(x.currentTarget.value)}))})]}),e("label",{children:[e("span",{children:"Snap length (bytes)"}),e("input",{type:"number",min:64,max:9e3,step:64,value:f.snapLength,disabled:p,onChange:x=>r(R=>({...R,snapLength:Number(x.currentTarget.value)}))})]})]}),e("p",{class:"field-hint",children:["Approx. ",Ht(S)," /"," ",L.toLocaleString()," packets. VPP keeps up to the computed packet count and writes the pcap under the host\u2019s ",e("code",{children:"/tmp"}),"; only traffic on the selected interface is captured."]}),e("div",{class:"capture-actions",children:[p?e("button",{class:"button primary",type:"button",disabled:h,onClick:()=>{ne()},children:h?"Stopping\u2026":"Stop capture"}):e("button",{class:"button primary",type:"button",disabled:!N||u||b,onClick:()=>{G()},children:u?"Starting\u2026":"Start capture"}),s?.file?e("span",{class:"capture-current-file",children:[p?"Writing":"Last capture",": ",e("code",{children:s.file})]}):null]}),s?e("div",{class:p?"capture-status active":"capture-status",children:[e("div",{class:"capture-status-head",children:[e("strong",{children:p?"Capture running":"No capture running"}),s.interface?e("span",{children:s.interface}):null]}),e("div",{class:"capture-status-grid",children:[e("span",{children:[e("b",{children:"Direction"}),Li(s.direction)]}),e("span",{children:[e("b",{children:"Size"}),Ht(s.size)]}),e("span",{children:[e("b",{children:p?"Buffer":"Captured"}),p?`${Ye(s.buffered)} / ${Ye(s.max_packets)} packets`:`${Ye(s.packets)} packets`]}),e("span",{children:[e("b",{children:"Limit"}),Ye(s.max_packets)," packets \xB7 ",Ht(s.max_bytes)]}),e("span",{children:[e("b",{children:"Started"}),yn(s.started_at)]}),s.stopped_at?e("span",{children:[e("b",{children:"Stopped"}),yn(s.stopped_at)]}):null]}),s.raw?e("details",{class:"capture-raw",children:[e("summary",{children:"VPP output"}),e("pre",{children:s.raw})]}):null]}):null,e("div",{class:"capture-files",children:[e("div",{class:"capture-files-head",children:[e("h3",{children:"Captures"}),e("span",{children:[a.length," file",a.length===1?"":"s"]})]}),a.length===0?e("p",{class:"field-hint",children:"No captures yet. Start a capture, then stop it to inspect the pcap."}):e("div",{class:"table-wrap",children:e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{children:"File"}),e("th",{children:"Size"}),e("th",{children:"Modified"}),e("th",{children:"Actions"})]})}),e("tbody",{children:a.map(x=>e("tr",{children:[e("td",{children:e("code",{children:x.name})}),e("td",{children:Ht(x.size)}),e("td",{children:yn(x.modified)}),e("td",{children:e("div",{class:"capture-file-actions",children:[e("a",{class:"button ghost",href:fa(x.name),download:!0,children:"Download"}),e("button",{class:"button ghost danger",type:"button",disabled:I===x.name||p&&s?.file===x.name,title:p&&s?.file===x.name?"Stop the capture before deleting it":void 0,onClick:()=>{J(x)},children:I===x.name?"Deleting\u2026":"Delete"})]})})]},x.name))})]})})]})]})}var Ii=/^\s*Packet\s+(\d+)\s*$/,ha=/^\s*\d{2}:\d{2}:\d{2}:\d+:\s+(\S+)/;function Ai(t){let n=t.replace(/\r/g,"").split(` -`),s=[],l=null;for(let a of n){let o=a.match(Ii);if(o){l&&s.push({...l,raw:l.lines.join(` -`).trim()}),l={number:Number(o[1]),nodes:[],lines:[a]};continue}if(!l)continue;l.lines.push(a);let f=a.match(ha);f&&l.nodes.push(f[1])}if(l&&s.push({...l,raw:l.lines.join(` -`).trim()}),s.length===0){let a=n.map(o=>o.match(ha)?.[1]).filter(o=>!!o);a.length>0&&s.push({number:1,nodes:a,raw:t.trim()})}return s}function Ni({packet:t}){return t.nodes.length===0?e("p",{class:"empty-state",children:"No VPP node path found for this packet."}):e("div",{class:"trace-path-panel",children:[e("div",{class:"trace-path-head",children:[e("strong",{children:["Packet ",t.number]}),e("span",{class:"muted",children:[t.nodes.length," nodes"]})]}),e("ol",{class:"trace-path","aria-label":`Packet ${t.number} node path`,children:t.nodes.map((n,s)=>e("li",{children:[e("span",{class:"trace-path-node","data-drop":n.includes("drop")?"true":void 0,children:n}),s{L(!0);try{let F=await V("/api/troubleshoot/trace/nodes"),W=Array.isArray(F)?F:Array.isArray(F.nodes)?F.nodes:[];g(W),N("")}catch(F){N(F instanceof Error?F.message:String(F))}finally{L(!1)}};z(()=>{t&&H()},[t]),z(()=>{n||m.length===0||s(m[0])},[n,m]);let G=async F=>{if(F.preventDefault(),!(r||!n.trim())){I(""),b(!0),f(null);try{let W=await V("/api/troubleshoot/trace",{method:"POST",body:JSON.stringify({node:n.trim(),count:Number(l)})});f({...W,raw:""})}catch(W){I(W instanceof Error?W.message:String(W))}finally{b(!1)}}},J=async()=>{I(""),u(!0);try{let F=await V("/api/troubleshoot/trace");f(W=>({...F,node:W?.node??n,count:W?.count??Number(l)}))}catch(F){I(F instanceof Error?F.message:String(F))}finally{u(!1)}},ne=We(()=>Ai(o?.raw??""),[o?.raw]);z(()=>{T(0)},[o?.raw]);let x=ne[p],R=async()=>{I(""),h(!0);try{await V("/api/troubleshoot/trace",{method:"DELETE"}),f(null)}catch(F){I(F instanceof Error?F.message:String(F))}finally{h(!1)}};return e("div",{class:"trace-panel",children:[e("form",{class:"trace-form",novalidate:!0,onSubmit:F=>{G(F)},children:[e("div",{class:"field trace-node-field",children:[e("span",{class:"field-label",children:"VPP graph node"}),e("select",{class:"trace-node-list",required:!0,value:n,onChange:F=>s(F.currentTarget.value),children:[e("option",{value:"",disabled:!0,children:"Select a VPP node\u2026"}),m.map(F=>e("option",{value:F,children:F},F))]}),e("span",{class:"field-hint",children:m.length>0?`${m.length} VPP node${m.length===1?"":"s"} available.`:S||"No VPP nodes loaded yet."}),e("button",{class:"row-action trace-refresh-nodes",type:"button",disabled:P,onClick:()=>{H()},children:P?"Loading nodes\u2026":"Refresh nodes"})]}),e("label",{class:"field trace-count-field",children:[e("span",{class:"field-label",children:"Packet count"}),e("input",{type:"number",required:!0,min:1,max:1e3,value:l,onInput:F=>a(F.currentTarget.value)}),e("span",{class:"field-hint",children:"Trace up to 1,000 packets."})]}),e("button",{class:"button primary",type:"submit",disabled:r||!n.trim()||Number(l)<1||Number(l)>1e3,children:r?"Starting\u2026":"Start trace"}),e("button",{class:"button ghost",type:"button",disabled:w,onClick:()=>{J()},children:w?"Loading\u2026":"Show trace"}),e("button",{class:"button ghost",type:"button",disabled:k,onClick:()=>{R()},children:k?"Clearing\u2026":"Clear"})]}),e("p",{class:"field-hint",children:["Start a trace, generate traffic through the selected node, then click ",e("strong",{children:"Show trace"}),"."]}),A?e("div",{class:"banner",role:"alert",children:A}):null,o?e("div",{class:"trace-result",children:[e("div",{class:"trace-result-head",children:[e("span",{children:o.node?`${o.node} \xB7 ${o.count||0} packets`:"VPP trace"}),o.captured_at?e("span",{class:"muted",children:new Date(o.captured_at).toLocaleString()}):null]}),ne.length>0?e("div",{class:"trace-visual",children:[e("label",{class:"field trace-packet-field",children:[e("span",{class:"field-label",children:"Packet"}),e("select",{value:String(p),onChange:F=>T(Number(F.currentTarget.value)),children:ne.map((F,W)=>e("option",{value:String(W),children:["Packet ",F.number," \xB7 ",F.nodes.length," nodes"]},`${F.number}-${W}`))}),e("span",{class:"field-hint",children:"Select a packet to draw its VPP node path."})]}),x?e(Ni,{packet:x}):null]}):e("p",{class:"empty-state",children:"No trace packets captured yet. Generate traffic and click Show trace."}),e("details",{class:"trace-raw",children:[e("summary",{children:"Raw trace text"}),e("pre",{class:"trace-output",children:o.raw||"No trace packets captured yet. Generate traffic and click Show trace."})]})]}):e("p",{class:"empty-state",children:"No trace loaded."})]})}function ga({table:t,routeID:n,interfaces:s,bviInterfaces:l,hostInterfaces:a,hostTaps:o,tables:f,onBack:r,onSaved:b}){let w=n!==void 0,[u,k]=d(w),[h,A]=d(!1),[I,m]=d(""),[g,P]=d(t.ipv6?"2001:db8::/64":"10.0.0.0/24"),[L,S]=d(""),[N,p]=d(t.ipv6?"2001:db8::1":"10.0.0.1"),[T,H]=d(""),[G,J]=d(""),[ne,x]=d(""),[R,F]=d(""),[W,Y]=d(""),[y,ae]=d(""),[E,M]=d(""),[se,ce]=d(""),[v,$]=d(""),[C,U]=d(""),[de,ie]=d(!1),[pe,ee]=d(!1),[ue,me]=d(""),[c,_]=d(""),[B,X]=d(""),[Z,fe]=d(!1),[Ie,le]=ke("route",n);z(()=>{document.title=`${w?"Edit":"Add"} route \xB7 ${t.name} \xB7 rookery`},[w,t.name]),z(()=>{if(n===void 0)return;let K=!1;return k(!0),V(`/api/route-tables/${t.id}/routes?ipv6=${t.ipv6}`).then($e=>{if(K)return;let O=(Array.isArray($e)?$e:[]).find(_e=>_e.id===n);if(!O){m("Route not found.");return}P(O.prefix),S(O.description||""),p(O.next_hop||""),O.interface_id?(H(String(O.interface_id)),J("")):(H(""),J(O.egress_interface||"")),x(O.preference===void 0?"":String(O.preference)),F(O.count===void 0?"":String(O.count)),Y(O.next_hop_table===void 0?"":String(O.next_hop_table)),ae(O.weight===void 0?"":String(O.weight)),M(O.udp_encap===void 0?"":String(O.udp_encap)),ce(O.ipv4_lookup_table===void 0?"":String(O.ipv4_lookup_table)),$(O.ipv6_lookup_table===void 0?"":String(O.ipv6_lookup_table)),U(O.mpls_lookup_table===void 0?"":String(O.mpls_lookup_table)),ie(!!O.resolve_via_host),ee(!!O.resolve_via_connected),me(O.rx_ipv4_interface||""),_(O.rx_ipv6_interface||""),X((O.out_labels||[]).join(" ")),fe(!!O.drop),m("")}).catch($e=>m($e instanceof Error?$e.message:String($e))).finally(()=>{K||k(!1)}),()=>{K=!0}},[n,t.id,t.ipv6]);let be=async K=>{if(K.preventDefault(),h||u)return;if(!g.trim()){m("Enter a destination prefix.");return}if(ue.trim()&&c.trim()){m("rx-ip4 and rx-ip6 are mutually exclusive.");return}if(!(N.trim()||T||G.trim()||Z||ue.trim()||c.trim()||W||E||se||v||C)){m("Enter a next hop, egress interface, rx interface, lookup option, or drop.");return}let O=T.startsWith("vpp:")?T.slice(4):"",_e=G.trim()||O,Ee=T&&!O&&!G.trim()?Number(T):0,q=B.split(/[\s,]+/).map(Ae=>Ae.trim()).filter(Boolean).map(Ae=>Math.trunc(Number(Ae))).filter(Ae=>Number.isFinite(Ae)&&Ae>=0),we=Ae=>{let ft=Ae.trim();if(!ft)return;let it=Math.max(0,Math.trunc(Number(ft)));return Number.isFinite(it)?it:void 0},Te=we(ne),xe=we(R),Oe=we(y),et=we(E),Ke=we(C),dt=W.trim(),Ve=se.trim(),Le=v.trim(),Je={ipv6:t.ipv6,prefix:g.trim(),description:L.trim(),next_hop:N.trim(),...Ee?{interface_id:Ee}:{},..._e?{egress_interface:_e}:{},...Te!==void 0?{preference:Te}:{},...xe!==void 0?{count:xe}:{},...dt?{next_hop_table:dt}:{},...Oe!==void 0?{weight:Oe}:{},...et!==void 0?{udp_encap:et}:{},...Ve?{ipv4_lookup_table:Ve}:{},...Le?{ipv6_lookup_table:Le}:{},...Ke!==void 0?{mpls_lookup_table:Ke}:{},resolve_via_host:de,resolve_via_connected:pe,rx_ipv4_interface:ue.trim(),rx_ipv6_interface:c.trim(),out_labels:q,drop:Z};A(!0),m("");try{let Ae;if(n===void 0)Ae=await V(`/api/route-tables/${t.id}/routes`,{method:"POST",body:JSON.stringify(Je)});else{let{ipv6:ft,...it}=Je;Ae=await V(`/api/routes/${n}`,{method:"PUT",body:JSON.stringify({id:n,...it})})}await Ce("route",Ae.id,Ae.prefix,Ie),b()}catch(Ae){m(Ae instanceof Error?Ae.message:String(Ae))}finally{A(!1)}},ve=K=>f.filter($e=>$e.ipv6===K&&$e.table_id!==0);return e("div",{class:"route-page",children:[e("header",{class:"route-page-head",children:e("div",{children:[e("button",{class:"back-link",type:"button",onClick:r,children:["\u2190 Routes \xB7 ",t.name]}),e("p",{class:"eyebrow",children:[t.ipv6?"IPv6":"IPv4"," FIB \xB7 table ",t.table_id]}),e("h2",{children:w?"Edit route":"Add route"}),e("p",{class:"panel-subtitle",children:["Route options map directly to the VPP ",e("code",{children:"ip route"})," command."]})]})}),e("form",{class:"route-page-form",onSubmit:K=>{be(K)},children:[u?e("p",{class:"field-hint",children:"Loading route\u2026"}):null,e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Prefix"}),e("input",{type:"text",required:!0,value:g,onInput:K=>P(K.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Next hop"}),e("input",{type:"text",value:N,onInput:K=>p(K.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Egress interface"}),e("select",{value:T,onChange:K=>H(K.currentTarget.value),children:[e("option",{value:"",children:"Resolve via table"}),s.map(K=>e("option",{value:String(K.id),children:K.name},K.id)),l.length>0?e("optgroup",{label:"Network gateways (BVI)",children:l.map(K=>e("option",{value:`vpp:${K}`,children:K},`bvi-${K}`))}):null,a.some(K=>K.in_vpp&&K.vpp_name)?e("optgroup",{label:"Host interfaces",children:a.filter(K=>K.in_vpp&&K.vpp_name).map(K=>e("option",{value:`vpp:${K.vpp_name}`,children:[K.vpp_name," (host)"]},`host-${K.name}`))}):null,o.length>0?e("optgroup",{label:"Host taps",children:o.map(K=>e("option",{value:`vpp:${K.name}`,children:K.name},`tap-${K.id}`))}):null]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Other VPP interface"}),e("input",{type:"text",placeholder:"host-eno4",value:G,onInput:K=>J(K.currentTarget.value)}),e("span",{class:"field-hint",children:"Overrides the picker when set."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Metric / preference"}),e("input",{type:"number",min:"0",max:"255",value:ne,onInput:K=>x(K.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Description"}),e("textarea",{rows:2,maxlength:1e3,value:L,onInput:K=>S(K.currentTarget.value)})]}),e("div",{class:"field field-wide",children:e(Se,{labels:Ie,onChange:le,disabled:h})})]}),e("section",{class:"route-advanced","aria-labelledby":"route-advanced-title",children:[e("h4",{class:"route-advanced-title",id:"route-advanced-title",children:"Advanced VPP route options"}),e("p",{class:"field-hint",children:["Leave an option blank to omit it from the VPP command. Next-hop and IP lookup tables use route-table names; choose ",e("code",{children:"default"}),"for table ",e("code",{children:"0"}),". MPLS lookup uses a numeric table ID."]}),e("div",{class:"route-advanced-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Count"}),e("input",{type:"number",min:"1",max:"100000",placeholder:"1",value:R,onInput:K=>F(K.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Next-hop table"}),e("select",{value:W,onChange:K=>Y(K.currentTarget.value),children:[e("option",{value:"",children:"Omit"}),e("option",{value:"default",children:"default (0)"}),ve(t.ipv6).map(K=>e("option",{value:K.name,children:[K.name," (",K.table_id,")"]},`nh-${K.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Weight"}),e("input",{type:"number",min:"0",max:"255",value:y,onInput:K=>ae(K.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"UDP encap"}),e("input",{type:"number",min:"0",value:E,onInput:K=>M(K.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv4 lookup table"}),e("select",{value:se,onChange:K=>ce(K.currentTarget.value),children:[e("option",{value:"",children:"Omit"}),e("option",{value:"default",children:"default (0)"}),ve(!1).map(K=>e("option",{value:K.name,children:[K.name," (",K.table_id,")"]},`v4-${K.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv6 lookup table"}),e("select",{value:v,onChange:K=>$(K.currentTarget.value),children:[e("option",{value:"",children:"Omit"}),e("option",{value:"default",children:"default (0)"}),ve(!0).map(K=>e("option",{value:K.name,children:[K.name," (",K.table_id,")"]},`v6-${K.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"MPLS lookup table ID"}),e("input",{type:"number",min:"0",value:C,onInput:K=>U(K.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"rx-ip4 interface"}),e("input",{type:"text",value:ue,onInput:K=>me(K.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"rx-ip6 interface"}),e("input",{type:"text",value:c,onInput:K=>_(K.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Out labels"}),e("input",{type:"text",placeholder:"100 200 300",value:B,onInput:K=>X(K.currentTarget.value)})]}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:de,onChange:K=>ie(K.currentTarget.checked)}),e("span",{children:"resolve-via-host"})]}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:pe,onChange:K=>ee(K.currentTarget.checked)}),e("span",{children:"resolve-via-connected"})]}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:Z,onChange:K=>fe(K.currentTarget.checked)}),e("span",{children:"drop"})]})]})]}),I?e("p",{class:"form-error",role:"alert",children:I}):null,e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",onClick:r,disabled:h,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:h||u,children:h?"Saving\u2026":w?"Save route":"Add route"})]})]})]})}function va({tables:t,onCancel:n,onSaved:s}){let l=Math.max(0,...t.filter(P=>!P.is_default).map(P=>P.table_id))+1,[a,o]=d(""),[f,r]=d(String(l)),[b,w]=d(!1),[u,k]=ke("route_table"),[h,A]=d(!1),[I,m]=d("");z(()=>{document.title="Create route table \xB7 rookery"},[]);let g=async P=>{if(P.preventDefault(),!h){m(""),A(!0);try{let L=await V("/api/route-tables",{method:"POST",body:JSON.stringify({name:a.trim(),table_id:Number(f),ipv6:b})});await Ce("route_table",L.id,L.name,u),s(L)}catch(L){m(L instanceof Error?L.message:String(L))}finally{A(!1)}}};return e("div",{class:"route-page",children:[e("header",{class:"route-page-head",children:e("div",{children:[e("button",{class:"back-link",type:"button",onClick:n,children:"\u2190 Route tables"}),e("p",{class:"eyebrow",children:"IP FIB / VRF"}),e("h2",{children:"Create route table"}),e("p",{class:"panel-subtitle",children:"Define an IPv4 or IPv6 VRF/FIB table. Table 0 is the built-in default VRF."})]})}),e("form",{class:"route-page-form",novalidate:!0,onSubmit:P=>{g(P)},children:[e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Name"}),e("input",{type:"text",required:!0,maxLength:63,placeholder:"tenant-blue",value:a,onInput:P=>o(P.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Table ID"}),e("input",{type:"number",required:!0,min:1,value:f,onInput:P=>r(P.currentTarget.value)}),e("span",{class:"field-hint",children:"Custom tables start at 1; table 0 is the default VRF."})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:b,onChange:P=>w(P.currentTarget.checked)}),e("span",{children:"IPv6 FIB table"})]}),e("span",{class:"field-hint",children:"The same table ID can be used separately for IPv4 and IPv6."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:u,onChange:k,disabled:h})})]}),I?e("p",{class:"form-error",role:"alert",children:I}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:h,onClick:n,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:h||!a.trim()||Number(f)<1,children:h?"Creating\u2026":"Create route table"})]})]})]})}function Di(t){let n=[];return(t.count||0)>1&&n.push(`count ${t.count}`),t.next_hop_table&&n.push(`next-hop-table ${t.next_hop_table}`),t.weight&&n.push(`weight ${t.weight}`),t.udp_encap&&n.push(`udp-encap ${t.udp_encap}`),t.ipv4_lookup_table&&n.push(`ip4-lookup-in-table ${t.ipv4_lookup_table}`),t.ipv6_lookup_table&&n.push(`ip6-lookup-in-table ${t.ipv6_lookup_table}`),t.mpls_lookup_table&&n.push(`mpls-lookup-in-table ${t.mpls_lookup_table}`),t.resolve_via_host&&n.push("resolve-via-host"),t.resolve_via_connected&&n.push("resolve-via-connected"),t.rx_ipv4_interface&&n.push(`rx-ip4 ${t.rx_ipv4_interface}`),t.rx_ipv6_interface&&n.push(`rx-ip6 ${t.rx_ipv6_interface}`),t.out_labels&&t.out_labels.length>0&&n.push(`out-labels ${t.out_labels.join(" ")}`),t.drop&&n.push("drop"),n.join(" \xB7 ")}function ya({table:t,onBack:n,onChanged:s}){let[l,a]=d([]),[o,f]=d([]),[r,b]=d(!0),[w,u]=d(!0),[k,h]=d(""),[A,I]=d(""),m=Ne("route"),g=`/route-tables/${t.id}/routes/new?ipv6=${t.ipv6}`,P=async()=>{b(!0);try{let p=await V(`/api/route-tables/${t.id}/routes?ipv6=${t.ipv6}`);a(Array.isArray(p)?p:[]),h("")}catch(p){h(p instanceof Error?p.message:String(p))}finally{b(!1)}},L=async()=>{u(!0);try{let p=await V(`/api/route-tables/${t.id}/fib?ipv6=${t.ipv6}`);f(Array.isArray(p)?p:[]),I("")}catch(p){I(p instanceof Error?p.message:String(p))}finally{u(!1)}};z(()=>{P()},[t.id,t.ipv6]),z(()=>{L()},[t.id,t.ipv6]),z(()=>{document.title=`Routes \xB7 ${t.name} \xB7 rookery`},[t.name]);let S=async p=>{if(window.confirm(`Delete route ${p.prefix}${p.next_hop?` via ${p.next_hop}`:""}?`)){h("");try{await V(`/api/routes/${p.id}`,{method:"DELETE"}),a(T=>T.filter(H=>H.id!==p.id)),await L(),s()}catch(T){h(T instanceof Error?T.message:String(T))}}},N=o.filter(p=>p.dynamic);return e("div",{class:"route-page",children:[e("header",{class:"route-page-head",children:[e("div",{children:[e("button",{class:"back-link",type:"button",onClick:n,children:"\u2190 Route tables"}),e("p",{class:"eyebrow",children:[t.ipv6?"IPv6":"IPv4"," FIB \xB7 table ",t.table_id,t.is_default?" \xB7 default VRF":""]}),e("h2",{children:["Routes \xB7 ",t.name]}),e("p",{class:"panel-subtitle",children:"Static routes are managed by Rookery. Dynamic routes are read directly from the live VPP FIB."})]}),e("button",{class:"button primary",type:"button",onClick:()=>Q(g),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add route"]})]}),e("section",{class:"route-page-section","aria-labelledby":"static-routes-title",children:[e("div",{class:"panel-head",children:e("div",{children:[e("h3",{id:"static-routes-title",children:"Static routes"}),e("p",{class:"field-hint",children:"Prefixes installed in this VRF by Rookery."})]})}),k?e("p",{class:"form-error",role:"alert",children:k}):null,e("div",{class:"table-wrap route-table-wrap",children:r?e("p",{class:"empty-state",children:"Loading routes\u2026"}):l.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Prefix"}),e("th",{scope:"col",children:"Next hop"}),e("th",{scope:"col",children:"Interface"}),e("th",{scope:"col",children:"Options"}),e("th",{scope:"col",children:"Preference"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:l.map(p=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name mono",children:p.prefix}),p.description?e("span",{class:"vm-description",children:p.description}):null,e(De,{entityType:"route",entityID:p.id,name:p.prefix,labels:m.labels[String(p.id)],onSaved:T=>m.setEntityLabels(String(p.id),T)})]}),e("td",{class:"mono",children:p.drop?"drop":p.next_hop||"attached"}),e("td",{class:"mono",children:p.egress_interface||p.interface_name||"resolved by table"}),e("td",{class:"mono",children:Di(p)||"\u2014"}),e("td",{class:"mono",children:p.preference||0}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>Q(`/route-tables/${t.id}/routes/${p.id}/edit?ipv6=${t.ipv6}`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{S(p)},children:"Delete"})]})})]},p.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2197"}),e("h3",{children:"No static routes"}),e("p",{children:"Add a route to this VRF."}),e("button",{class:"button primary",type:"button",onClick:()=>Q(g),children:"Add route"})]})})]}),e("section",{class:"fib-section route-page-section","aria-labelledby":"fib-routes-title",children:[e("div",{class:"panel-head fib-head",children:[e("div",{children:[e("h3",{id:"fib-routes-title",children:"Dynamic routes"}),e("p",{class:"field-hint",children:"Read-only entries from the live VPP FIB, including connected, DHCP, ND, and adjacency routes."})]}),e("button",{class:"button ghost",type:"button",disabled:w,onClick:()=>{L()},children:w?"Refreshing\u2026":"Refresh FIB"})]}),A?e("p",{class:"form-error",role:"alert",children:A}):w?e("p",{class:"empty-state",children:"Loading VPP FIB\u2026"}):N.length>0?e("div",{class:"table-wrap route-table-wrap",children:e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Prefix"}),e("th",{scope:"col",children:"Source"}),e("th",{scope:"col",children:"Paths"})]})}),e("tbody",{children:N.map(p=>e("tr",{children:[e("td",{class:"mono",children:p.prefix}),e("td",{children:e("span",{class:"fib-source",children:p.source_name})}),e("td",{children:(p.paths??[]).length>0?(p.paths??[]).map((T,H)=>e("div",{class:"fib-path",children:[e("span",{class:"fib-path-type",children:T.type})," ",e("span",{class:"mono",children:T.next_hop||"\u2014"}),T.interface_name?e("span",{children:[" via ",T.interface_name]}):null]},`${p.prefix}-${H}`)):e("span",{class:"muted",children:"no paths"})})]},`${p.ipv6?"v6":"v4"}-${p.prefix}`))})]})}):e("p",{class:"empty-state",children:"No dynamic routes in this table."})]})]})}function _a({name:t,label:n,hostAddresses:s,onClose:l}){let[a,o]=d(""),[f,r]=d(!1),[b,w]=d([]),[u,k]=d(!1),[h,A]=d(""),[I,m]=d(""),[g,P]=d(!0),[L,S]=d(!1),[N,p]=d(!1),[T,H]=d(""),G=te(null),J=te(null),ne=`/api/vpp/interfaces/${encodeURIComponent(t)}/addresses`,x=y=>{w(y.live_addresses||[]),k(!!y.dhcp_client_configured),A(y.dhcp_client_status||""),m(y.status_detail||"")};z(()=>{G.current?.showModal(),R()},[]);let R=async()=>{P(!0);try{let y=await V(ne);o((y.addresses||[]).join(` -`)),r(!!y.dhcp_client_enabled),x(y),H("")}catch(y){H(y instanceof Error?y.message:String(y))}finally{P(!1),J.current?.focus()}},F=async()=>{if(!L){S(!0);try{let y=await V(ne);x(y),H("")}catch(y){H(y instanceof Error?y.message:String(y))}finally{S(!1)}}},W=()=>{N||G.current?.close()},Y=async y=>{if(y.preventDefault(),N||g)return;p(!0),H("");let ae=a.split(/\r?\n/).map(E=>E.trim()).filter(Boolean);try{let E=await V(ne,{method:"PUT",body:JSON.stringify({addresses:ae,dhcp_client_enabled:f})});o((E.addresses||[]).join(` -`)),r(!!E.dhcp_client_enabled),x(E),oe(`Saved configuration for ${t}`)}catch(E){H(E instanceof Error?E.message:String(E))}finally{p(!1)}};return e("dialog",{ref:G,id:"vpp-addresses-dialog","aria-labelledby":"vpp-addresses-title",onClose:l,onClick:y=>{y.target===y.currentTarget&&W()},onCancel:y=>{N&&y.preventDefault()},children:e("form",{onSubmit:y=>{Y(y)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"VPP interface addresses"}),e("h2",{id:"vpp-addresses-title",children:["Addresses \xB7 ",t]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:W,children:"\xD7"})]}),e("div",{class:"startup-script-status",children:[e("span",{class:"status-badge","data-status":"running",children:n||"VPP interface"}),e("span",{class:"field-hint",children:"IPv4 and IPv6 CIDRs are applied directly to this VPP interface."})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:f,onChange:y=>r(y.currentTarget.checked)}),e("span",{children:"Enable DHCP client (IPv4)"})]}),e("span",{class:"field-hint",children:"VPP requests an IPv4 lease from the network. This can be combined with static addresses."})]}),s?e("div",{class:"vpp-address-live field-wide",children:[e("div",{class:"vpp-address-live-head",children:e("span",{class:"field-label",children:"Host side addresses"})}),s.length>0?e("div",{class:"vpp-live-addresses mono",children:s.map(y=>e("span",{children:y},y))}):e("p",{class:"field-hint",children:"No host-side address assigned."})]}):null,e("div",{class:"vpp-address-live field-wide",children:[e("div",{class:"vpp-address-live-head",children:[e("span",{class:"field-label",children:"VPP side addresses"}),e("div",{class:"actions",children:[e("span",{class:"status-badge","data-status":u?"running":"disabled",children:["DHCP client ",u?"configured":"not configured"]}),e("button",{class:"row-action",type:"button",disabled:L||N,onClick:()=>{F()},children:L?"Refreshing\u2026":"Refresh status"})]})]}),I?e("p",{class:"field-hint",children:I}):b.length>0?e("div",{class:"vpp-live-addresses mono",children:b.map(y=>e("span",{children:y},y))}):e("p",{class:"field-hint",children:["No live address assigned yet",f?"; waiting for a DHCP lease.":"."]}),h?e("details",{class:"startup-script-output",children:[e("summary",{children:"DHCP client status"}),e("pre",{children:h})]}):null]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Addresses"}),e("textarea",{ref:J,class:"startup-script-editor",rows:8,spellcheck:!1,placeholder:`10.20.0.10/24 -fd00:20::10/64`,value:a,onInput:y=>o(y.currentTarget.value)}),e("span",{class:"field-hint",children:"One address per line, using CIDR notation. Leave empty to remove all Rookery-managed addresses. Existing addresses managed by other Rookery features are not shown here."})]}),T?e("p",{class:"form-error",role:"alert",children:T}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:N,onClick:W,children:"Close"}),e("button",{class:"button primary",type:"submit",disabled:N||g,children:N?"Saving\u2026":"Save addresses"})]})]})})}function wa({name:t,label:n,tables:s,onClose:l,onSaved:a}){let[o,f]=d("0"),[r,b]=d("0"),[w,u]=d(!0),[k,h]=d(!1),[A,I]=d(""),m=te(null),g=`/api/vpp/interfaces/${encodeURIComponent(t)}/route-tables`;z(()=>{m.current?.showModal(),P()},[]);let P=async()=>{u(!0);try{let p=await V(g);f(String(p.ipv4_table_id??0)),b(String(p.ipv6_table_id??0)),I("")}catch(p){I(p instanceof Error?p.message:String(p))}finally{u(!1)}},L=()=>{k||m.current?.close()},S=async p=>{if(p.preventDefault(),!(w||k)){h(!0),I("");try{let T=await V(g,{method:"PUT",body:JSON.stringify({ipv4_table_id:Number(o),ipv6_table_id:Number(r)})});a(T)}catch(T){I(T instanceof Error?T.message:String(T))}finally{h(!1)}}},N=p=>s.filter(T=>T.ipv6===p&&T.table_id!==0);return e("dialog",{ref:m,id:"vpp-interface-route-tables-dialog","aria-labelledby":"vpp-interface-route-tables-title",onClose:l,onClick:p=>{p.target===p.currentTarget&&L()},onCancel:p=>{k&&p.preventDefault()},children:e("form",{novalidate:!0,onSubmit:p=>{S(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Interface FIB binding"}),e("h2",{id:"vpp-interface-route-tables-title",children:["Route tables \xB7 ",t]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:L,children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[n,". Table 0 is the default VRF. Saving an explicit binding, including default (0), overrides any automatic network VRF for a host tap and may affect reachability."]}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv4 FIB"}),e("select",{value:o,disabled:w||k,onChange:p=>f(p.currentTarget.value),children:[e("option",{value:"0",children:"default (0)"}),N(!1).map(p=>e("option",{value:String(p.table_id),children:[p.name," (",p.table_id,")"]},`v4-${p.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv6 FIB"}),e("select",{value:r,disabled:w||k,onChange:p=>b(p.currentTarget.value),children:[e("option",{value:"0",children:"default (0)"}),N(!0).map(p=>e("option",{value:String(p.table_id),children:[p.name," (",p.table_id,")"]},`v6-${p.id}`))]})]})]}),A?e("p",{class:"form-error",role:"alert",children:A}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:k,onClick:L,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:w||k,children:k?"Applying\u2026":"Apply route tables"})]})]})})}var wn=["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"],ka=92,Ca=104,yt=60,Sa=900,xi=220,Vi=/^(?:vhost-user-input|tap-input|af-packet-input|interface-output|interface-\d+-(?:output|tx)(?:-deleted)?|(?:bvi|host|vm|vnf)[^ ]*-(?:output|tx)(?:-deleted)?)$/;function Mi(t){let n=/^(.*?)-(?:output|tx)(?:-deleted)?$/.exec(t);return n?n[1]:t}function $i(t){return t==="vhost-user-input"?"vhost-user input":t==="tap-input"?"tap input":t==="af-packet-input"?"af-packet input":t==="interface"?"interface (generic)":/^interface-\d+$/.test(t)?`${t} (deleted)`:t.startsWith("vm-")?`${t} (VM interface)`:/^vnf/.test(t)?`${t} (VNF tap)`:/^bvi/.test(t)?`${t} (BVI)`:t.startsWith("host-")?`${t} (host NIC)`:t}function Ri(t){let n=new Map;for(let s of t.nodes){if(!s.isInterface)continue;let l=Mi(s.id),a=n.get(l);a?a.push(s.id):n.set(l,[s.id])}return Array.from(n.entries()).map(([s,l])=>({id:s,label:$i(s),nodeIDs:l})).sort((s,l)=>s.label.localeCompare(l.label))}function Fi(t){let n=[],s=(l,a,o)=>{let f=t.nodes.filter(r=>o(r.id)).map(r=>r.id);f.length>0&&n.push({id:l,label:a,nodeIDs:f})};return s("feature:acl","ACL plugin",l=>l.startsWith("acl-plugin")),s("feature:nat","NAT44",l=>l.startsWith("nat")),s("feature:dhcp","DHCP",l=>l.includes("dhcp")),n}function Ta(t){let n=new Map;for(let s of t.nodes)n.set(s.id,new Set([s.id]));for(let s of t.edges)n.get(s.source)?.add(s.target),n.get(s.target)?.add(s.source);return n}function Pa(t){let n=Array.isArray(t.nodes)?t.nodes:[],s=Array.isArray(t.edges)?t.edges:[],l={nodes:[],edges:[],width:Sa,height:420,maxRank:0,maxLayerCount:0,backwardCount:0,interfaceCount:0};if(n.length===0)return l;let a=new Map;n.forEach((C,U)=>a.set(C.id,U));let o=n.length,f=Array.from({length:o},()=>[]),r=Array.from({length:o},()=>[]),b=[],w=new Set;for(let C of s){let U=a.get(C.source),de=a.get(C.target);if(U===void 0||de===void 0)continue;let ie=`${U}\0${de}`;w.has(ie)||(w.add(ie),b.push({sourceIndex:U,targetIndex:de,source:C.source,target:C.target}),U!==de&&(f[U].push(de),r[de].push(U)))}let u=new Set;for(let C=0;C0&&f[C].every(U=>u.has(U))&&k.add(C);let h=[];for(let C=0;CA.has(U))),m.set(C,r[C].filter(U=>A.has(U)));let g=new Map,P=new Set,L=(C,U)=>{let de=[C];P.add(C),g.set(C,U);for(let ie=0;ie(m.get(C)?.length??0)===0);S.length===0&&h.length>0&&S.push(h[0]);for(let C of S)P.has(C)||L(C,0);for(let C of h)P.has(C)||L(C,0);let N=0;for(let C of g.values())N=Math.max(N,C);let p=N+1,T=N+(k.size>0?2:1),H=new Array(o).fill(0);for(let C of h)H[C]=g.get(C)??0;for(let C of k)H[C]=p;for(let C of u)H[C]=T;let G=0;for(let C of H)G=Math.max(G,C);let J=Array.from({length:G+1},()=>[]);for(let C=0;Cn[C].label||n[C].id;J.forEach(C=>{C.sort((U,de)=>ne(U).localeCompare(ne(de)))});let x=new Array(o).fill(0),R=()=>{J.forEach(C=>{C.forEach((U,de)=>{x[U]=de})})};R();let F=C=>{let U=J[H[C]];return U.length<=1?.5:x[C]/(U.length-1)};for(let C=0;C<5;C+=1){for(let U=1;U<=G;U+=1){let de=J[U],ie=new Map;for(let pe of de){let ee=r[pe].filter(ue=>H[ue]0?ee.reduce((ue,me)=>ue+F(me),0)/ee.length:F(pe))}de.sort((pe,ee)=>(ie.get(pe)??0)-(ie.get(ee)??0)||ne(pe).localeCompare(ne(ee))),R()}for(let U=G-1;U>=0;U-=1){let de=J[U],ie=new Map;for(let pe of de){let ee=f[pe].filter(ue=>H[ue]>U);ie.set(pe,ee.length>0?ee.reduce((ue,me)=>ue+F(me),0)/ee.length:F(pe))}de.sort((pe,ee)=>(ie.get(pe)??0)-(ie.get(ee)??0)||ne(pe).localeCompare(ne(ee))),R()}}let W=Math.max(...J.map(C=>C.length),1),Y=Math.max(Sa,(W-1)*ka+yt*2),y=Math.max(420,G*Ca+yt*2),ae=new Array(o).fill(Y/2),E=new Array(o).fill(yt);J.forEach((C,U)=>{let de=Math.min(Y-yt*2,Math.max(0,(C.length-1)*ka)),ie=Y/2-de/2;C.forEach((pe,ee)=>{ae[pe]=C.length===1?Y/2:ie+de*ee/(C.length-1),E[pe]=yt+U*Ca})});let M=n.map(C=>Number(C.width)||0),se=Math.min(...M),ce=Math.max(...M),v=n.map((C,U)=>{let de=Number(C.fill)||0,ie=ce>se?(M[U]-se)/(ce-se):.5;return{id:C.id,label:C.label||C.id,x:ae[U],y:E[U],radius:7+ie*11,color:de>=1&&de<=wn.length?wn[de-1]:"#64748b",rank:H[U],isInterface:Vi.test(C.id)}}),$=b.map(C=>({source:C.source,target:C.target,backward:H[C.sourceIndex]>=H[C.targetIndex]}));return{nodes:v,edges:$,width:Y,height:y,maxRank:G,maxLayerCount:W,backwardCount:$.filter(C=>C.backward).length,interfaceCount:v.filter(C=>C.isInterface).length}}function _n(t,n,s){return Math.min(s,Math.max(n,t))}function Ea({graph:t}){let n=We(()=>Pa(t),[t]),[s,l]=d(""),a=We(()=>Ri(n),[n]),o=We(()=>Fi(n),[n]),f=We(()=>[...a,...o],[a,o]),r=We(()=>Ta(n),[n]),b=We(()=>{if(!s)return null;let v=f.find(C=>C.id===s);if(!v)return null;let $=new Set(v.nodeIDs);for(let C of v.nodeIDs)for(let U of r.get(C)??[])$.add(U);return $},[s,f,r]),w=We(()=>b?{...t,nodes:t.nodes.filter(v=>b.has(v.id)),edges:t.edges.filter(v=>b.has(v.source)&&b.has(v.target))}:t,[t,b]),u=We(()=>Pa(w),[w]),[k,h]=d(()=>({x:0,y:0,w:u.width,h:u.height})),[A,I]=d(!1),[m,g]=d(null),[P,L]=d(!1),S=te(null),N=te(null);z(()=>{s&&!f.some(v=>v.id===s)&&l("")},[f,s]),z(()=>{N.current=null,I(!1),g(null),h({x:0,y:0,w:u.width,h:u.height})},[u]);let p=v=>{let{x:C,y:U,w:de,h:ie}=v;return de>=u.width?C=(u.width-de)/2:C=_n(C,-40,u.width-de+40),ie>=u.height?U=(u.height-ie)/2:U=_n(U,-40,u.height-ie+40),{x:C,y:U,w:de,h:ie}},T=(v,$,C)=>{h(U=>{let de=Math.max(xi,u.width*.06),ie=u.width*1.08,ee=_n(U.w/v,de,ie)/U.w,ue=U.w*ee,me=U.h*ee;return p({x:$-($-U.x)*ee,y:C-(C-U.y)*ee,w:ue,h:me})})},H=(v,$)=>{let C=S.current;if(!C)return{x:0,y:0};let U=C.getScreenCTM();if(!U)return{x:0,y:0};let de=C.createSVGPoint();de.x=v,de.y=$;let ie=de.matrixTransform(U.inverse());return{x:ie.x,y:ie.y}},G=()=>{h({x:0,y:0,w:u.width,h:u.height})},J=v=>{v.preventDefault();let $=H(v.clientX,v.clientY);T(Math.exp(-v.deltaY*.0015),$.x,$.y)},ne=v=>{if(v.pointerType==="mouse"&&v.button!==0)return;let $=S.current;if(!$)return;let C=$.getScreenCTM();C&&(v.preventDefault(),$.setPointerCapture(v.pointerId),N.current={pointerId:v.pointerId,startX:v.clientX,startY:v.clientY,startView:k,scaleX:C.a||1,scaleY:C.d||1},I(!0))},x=v=>{let $=N.current;if(!$||$.pointerId!==v.pointerId)return;let C=($.startX-v.clientX)/$.scaleX,U=($.startY-v.clientY)/$.scaleY;h(p({...$.startView,x:$.startView.x+C,y:$.startView.y+U}))},R=v=>{let $=N.current;if(!$||$.pointerId!==v.pointerId)return;N.current=null,I(!1);let C=S.current;C?.hasPointerCapture(v.pointerId)&&C.releasePointerCapture(v.pointerId)},F=We(()=>new Map(u.nodes.map(v=>[v.id,v])),[u]),W=We(()=>Ta(u),[u]);if(u.nodes.length===0)return e("div",{class:"empty vpp-graph-empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"No active VPP nodes"}),e("p",{children:"Generate traffic or refresh after VPP has processed packets since the last stats clear."})]});let y=u.nodes.length<=40||P||m!==null,ae=m?W.get(m)??new Set([m]):null,E=m&&ae?Math.max(0,ae.size-1):0,M=Math.max(9,Math.min(26,k.w/70)),se=Math.max(1,Math.round(u.width/k.w*100)),ce=m?F.get(m):null;return e("div",{class:"vpp-graph-wrap",children:[e("div",{class:"vpp-graph-meta",children:[e("span",{children:[u.nodes.length," active node",u.nodes.length===1?"":"s"," \xB7 ",u.edges.length," ","edge",u.edges.length===1?"":"s",u.backwardCount>0?` \xB7 ${u.backwardCount} feedback`:"",u.interfaceCount>0?` \xB7 ${u.interfaceCount} interface${u.interfaceCount===1?"":"s"}`:""]}),e("span",{class:"vpp-graph-legend",children:[e("i",{class:"vpp-legend-node"}),"node = cycles/packet",e("i",{class:"vpp-legend-packet"}),"color = packets/call",e("i",{class:"vpp-legend-interface"}),"ring = interface",ce?` \xB7 ${ce.label}${ce.isInterface?" (interface)":""} \xB7 ${E} connected`:" \xB7 hover to isolate connections"]})]}),e("div",{class:"vpp-graph-toolbar",children:[e("span",{class:"vpp-graph-hint",children:["Inputs at top \xB7 outputs at bottom \xB7 drag to pan \xB7 scroll to zoom",u.backwardCount>0?" \xB7 dashed edge = feedback/self-loop":""]}),e("div",{class:"vpp-graph-controls",children:[f.length>0?e("label",{class:"vpp-interface-picker",children:[e("span",{children:"Focus"}),e("select",{value:s,title:"Show only nodes related to this interface or packet feature",onChange:v=>l(v.currentTarget.value),children:[e("option",{value:"",children:"All nodes"}),a.length>0?e("optgroup",{label:"Interfaces",children:a.map(v=>e("option",{value:v.id,children:v.label},v.id))}):null,o.length>0?e("optgroup",{label:"Packet features",children:o.map(v=>e("option",{value:v.id,children:v.label},v.id))}):null]})]}):null,e("div",{class:"vpp-graph-zoom",children:[e("button",{type:"button","aria-label":"Zoom out",title:"Zoom out",onClick:()=>T(1/1.35,k.x+k.w/2,k.y+k.h/2),children:"\u2212"}),e("span",{children:[se,"%"]}),e("button",{type:"button","aria-label":"Zoom in",title:"Zoom in",onClick:()=>T(1.35,k.x+k.w/2,k.y+k.h/2),children:"+"}),e("button",{type:"button",onClick:G,children:"Fit"}),u.nodes.length>40?e("button",{type:"button","aria-pressed":P,class:P?"is-active":"",onClick:()=>L(v=>!v),children:"Labels"}):null]})]})]}),e("svg",{ref:S,class:`vpp-graph${A?" is-panning":""}`,viewBox:`${k.x} ${k.y} ${k.w} ${k.h}`,role:"img","aria-label":"Active VPP packet processing graph, inputs at top and outputs at bottom",onWheel:J,onPointerDown:ne,onPointerMove:x,onPointerUp:R,onPointerCancel:R,onLostPointerCapture:()=>{N.current=null,I(!1)},children:[e("defs",{children:e("marker",{id:"vpp-graph-arrow",viewBox:"0 0 10 10",refX:"9",refY:"5",markerWidth:"6",markerHeight:"6",orient:"auto-start-reverse",children:e("path",{d:"M 0 0 L 10 5 L 0 10 z",fill:"#64748b"})})}),e("g",{class:"vpp-graph-edges",children:u.edges.map((v,$)=>{let C=F.get(v.source),U=F.get(v.target);if(!C||!U)return null;let de=m!==null&&(v.source===m||v.target===m),ie=m!==null&&!de;if(v.source===v.target){let fe=C.radius+6;return e("path",{d:`M ${C.x-C.radius} ${C.y} C ${C.x-fe*2.2} ${C.y-fe*2.4}, ${C.x+fe*2.2} ${C.y-fe*2.4}, ${C.x+C.radius} ${C.y}`,class:`vpp-edge vpp-edge-back${de?" vpp-edge-active":""}${ie?" vpp-edge-hidden":""}`,markerEnd:"url(#vpp-graph-arrow)",vectorEffect:"non-scaling-stroke"},`${v.source}->${v.target}-${$}`)}let pe=U.x-C.x,ee=U.y-C.y,ue=Math.max(1,Math.sqrt(pe*pe+ee*ee)),me=C.x+pe/ue*(C.radius+2),c=C.y+ee/ue*(C.radius+2),_=U.x-pe/ue*(U.radius+5),B=U.y-ee/ue*(U.radius+5),X=B>=c?1:-1,Z=Math.max(24,Math.min(80,Math.abs(B-c)*.45));return e("path",{d:`M ${me} ${c} C ${me} ${c+X*Z}, ${_} ${B-X*Z}, ${_} ${B}`,class:`vpp-edge${v.backward?" vpp-edge-back":""}${de?" vpp-edge-active":""}${ie?" vpp-edge-hidden":""}`,markerEnd:"url(#vpp-graph-arrow)",vectorEffect:"non-scaling-stroke"},`${v.source}->${v.target}-${$}`)})}),e("g",{class:"vpp-graph-nodes",children:u.nodes.map(v=>{let $=v.x<160?"start":v.x>u.width-160?"end":"middle",C=$==="start"?v.x-v.radius:$==="end"?v.x+v.radius:v.x,U=ae===null||ae.has(v.id);return e("g",{class:`${y?"vpp-node":"vpp-node vpp-node-hover-label"}${U?"":" vpp-node-hidden"}`,onMouseEnter:()=>g(v.id),onMouseLeave:()=>g(null),children:[v.isInterface?e("circle",{class:"vpp-interface-ring",cx:v.x,cy:v.y,r:v.radius+4,vectorEffect:"non-scaling-stroke"}):null,e("circle",{cx:v.x,cy:v.y,r:v.radius,fill:v.color,vectorEffect:"non-scaling-stroke",children:e("title",{children:[v.label,v.isInterface?" (interface)":""]})}),e("text",{x:C,y:v.y+v.radius+M*1.25,textAnchor:$,style:{fontSize:`${M}px`},children:v.label})]},v.id)})})]}),e("div",{class:"vpp-graph-palette","aria-hidden":"true",children:[e("span",{children:"packets/call"}),wn.map((v,$)=>e("i",{style:{background:v},title:`${$+1}`},v)),e("span",{class:"vpp-graph-palette-note",children:"YlOrRd"})]})]})}function Ot(t,n){return`pipe${t.instance}.${n}`}function pt(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function Ut(t){let n=Math.trunc(Number(t)||0);return n>=1024**3?`${Math.trunc(n/1024**3)} GB`:n>=1024**2?`${Math.trunc(n/1024**2)} MB`:n>=1024?`${Math.trunc(n/1024)} KB`:`${n} B`}var kn=!1,La=0,Ia=[{id:"hosts",label:"Host interfaces"},{id:"hosttaps",label:"Host taps"},{id:"networks",label:"Networks"},{id:"pipes",label:"BD pipes"},{id:"interfaces",label:"Interfaces"},{id:"acl",label:"ACLs"},{id:"routes",label:"Route tables"},{id:"troubleshoot",label:"Troubleshoot"}];function Aa(){let[t,n]=d([]),[s,l]=d([]),[a,o]=d([]),[f,r]=d([]),[b,w]=d([]),[u,k]=d([]),[h,A]=d([]),[I,m]=d([]),[g,P]=d(!1),[L,S]=d(!0),[N,p]=d(""),[T,H]=d(""),[G,J]=d(""),[ne,x]=d(!1),[R,F]=d(""),[W,Y]=d(""),[y,ae]=d(""),[E,M]=d(!1),[se,ce]=d(""),[v,$]=d(""),[C,U]=d(""),[de,ie]=d(!1),[pe,ee]=d(null),[ue,me]=d(!1),[c,_]=d(null),[B,X]=d(null),[Z,fe]=d(null),[Ie,le]=d(null),[be,ve]=d(null),[K,$e]=d([]),[O,_e]=d(""),[Ee,q]=d(!1),[we,Te]=d(null),[xe,Oe]=d(null),[et,Ke]=d(null),[dt,Ve]=d(null),[Le,Je]=d("hosts"),[Ae,ft]=d(null),[it,Dn]=d(""),[Gt,xn]=d(!1),Kt=te(!1),Vn=te(0),[ro,lo]=d([]),[Jt,Mn]=d(""),[$n,Rn]=d(!1),[co,uo]=d({}),[po,Fn]=d({}),[kt,Ct]=d(""),Xt=te(!1),Zt=te(null),Bn=Ne("network"),Hn=Ne("network_pipe"),On=Ne("host_interface"),Un=Ne("host_tap"),qn=Ne("interface"),Wn=Ne("acl"),zn=Ne("route_table"),tt=ze();z(()=>{let[i,D,j]=tt.segments;if(D!=="hosttaps"&&M(!1),D!=="pipes"&&x(!1),D!=="hosts"&&D!=="hosttaps"&&fe(null),i==="networks"){if(me(!1),_(null),X(null),le(null),ve(null),q(!1),Te(null),Ke(null),D==="new"){ee(null),ie(!0),Je("networks");return}if(D&&j==="edit"){let re=t.find(he=>String(he.id)===D);if(re){ee(re),ie(!0),Je("networks");return}}ie(!1),ee(null),D&&Ia.some(re=>re.id===D)?Je(D):Je("networks");return}if(ie(!1),ee(null),i==="interfaces"){if(Je("interfaces"),q(!1),Te(null),D==="new"){me(!0),_(null),X(null),ve(null),Ke(null);return}let re=b.find(he=>String(he.id)===D);me(!1),X(null),_(re&&j==="attach"?re:null),ve(re&&j==="acls"?re:null),Ke(re&&j==="route-tables"?re:null);return}if(i==="acls"){if(Je("acl"),me(!1),_(null),X(null),ve(null),Ke(null),q(!1),Te(null),D==="new"){le({acl:null});return}let re=u.find(he=>String(he.id)===D);le(re&&j==="edit"?{acl:re}:null);return}if(i==="route-tables"){if(Je("routes"),me(!1),_(null),X(null),le(null),ve(null),Ke(null),D==="new"){q(!0),Te(null);return}if(q(!1),j==="routes"||!j){let re=tt.query.get("ipv6")==="true",he=K.find(He=>String(He.id)===D&&He.ipv6===re);Te(he??null),he?tt.segments[3]==="new"?Oe({table:he}):tt.segments[3]&&tt.segments[4]==="edit"?Oe({table:he,routeID:Number(tt.segments[3])}):Oe(null):Oe(null)}else Te(null),Oe(null);return}me(!1),Oe(null),_(null),X(null),le(null),ve(null),q(!1),Te(null),Ke(null)},[tt.path,tt.search,b,u,t,K]);let Be=i=>{i.networks&&(n(i.networks),window.dispatchEvent(new CustomEvent("rookery:networks-changed",{detail:i.networks}))),i.interfaces&&(w(i.interfaces),window.dispatchEvent(new CustomEvent("rookery:interfaces-changed",{detail:i.interfaces}))),i.acls&&k(i.acls),i.vms&&A(i.vms),i.vnfs&&m(i.vnfs)},Gn=async(i=!0)=>{try{let D=await V("/api/networks"),j=Array.isArray(D)?D:[];j.sort((re,he)=>re.name.localeCompare(he.name,void 0,{sensitivity:"base"})),Be({networks:j}),i&&p("")}catch(D){i&&p(D instanceof Error?D.message:String(D))}},fo=async(i=!0)=>{try{let D=await V("/api/network-pipes"),j=Array.isArray(D)?D:[];j.sort((re,he)=>re.id-he.id),l(j),i&&H("")}catch(D){i&&H(D instanceof Error?D.message:String(D))}},jt=async(i=!0)=>{try{let D=await V("/api/host/interfaces"),j=Array.isArray(D)?D:[];j.sort((re,he)=>re.name.localeCompare(he.name,void 0,{sensitivity:"base"})),o(j),i&&F("")}catch(D){i&&F(D instanceof Error?D.message:String(D))}},mo=async(i=!0)=>{try{let D=await V("/api/host/taps"),j=Array.isArray(D)?D:[];j.sort((re,he)=>re.name.localeCompare(he.name,void 0,{sensitivity:"base"})),r(j),i&&Y("")}catch(D){i&&Y(D instanceof Error?D.message:String(D))}},ho=async i=>{if(window.confirm(`Delete host tap ${i.name}? The host will lose its connection to ${i.network_name}.`)){ae(i.name),Y("");try{await V(`/api/host/taps/${i.id}`,{method:"DELETE"}),r(D=>D.filter(j=>j.id!==i.id)),oe(`Deleted host tap ${i.name}`)}catch(D){let j=D instanceof Error?D.message:String(D);Y(`Could not delete ${i.name}: ${j}`),oe(`Could not delete host tap ${i.name}: ${j}`,!0)}finally{ae("")}}},bo=async i=>{if(window.confirm(`Bring ${i.name} into VPP? +`);return e("dialog",{ref:m,id:"interface-acl-dialog","aria-labelledby":"interface-acl-title",onClose:l,onClick:p=>{p.target===p.currentTarget&&y()},onCancel:p=>{C&&p.preventDefault()},children:e("form",{novalidate:!0,onSubmit:p=>{P(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Interface policy"}),e("h2",{id:"interface-acl-title",children:"Assign ACLs"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:y,children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.name," \xB7 ",t.network_name," \xB7 ",t.mac]}),e("div",{class:"form-grid",children:[e(ls,{label:"From interface",hint:"Applied to packets entering the interface, in this order. VPP evaluates the first matching rule across the list.",acls:n,selected:r,onChange:b}),e(ls,{label:"To interface",hint:"Applied to packets leaving the interface, in this order.",acls:n,selected:w,onChange:d}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Full ACL preview"}),e("pre",{class:"acl-preview",children:N})]})]}),A?e("p",{class:"form-error",role:"alert",children:A}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:C,onClick:y,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:C||!a,title:a?"":"VPP must be ready to apply interface ACLs.",children:C?"Applying\u2026":"Apply ACLs"})]})]})})}var Pi="[A-Za-z0-9][A-Za-z0-9._\\-]*";function ds({networks:t,vms:n,onClose:a,onSaved:l}){let[s,o]=c(""),[f,r]=c(t[0]?String(t[0].id):""),[b,w]=c(""),[d,C]=ke("interface"),[h,A]=c(!1),[I,m]=c(""),y=ne(null),P=ne(null),L=ne(null);G(()=>{y.current?.showModal(),P.current?.focus()},[]);let S=()=>{h||y.current?.close()},N=async p=>{if(p.preventDefault(),!(h||!L.current?.reportValidity())){m(""),A(!0);try{let T=await V("/api/interfaces",{method:"POST",body:JSON.stringify({name:s.trim(),network_id:Number(f),attached_vm_id:b||null})});await Ce("interface",T.id,T.name,d),l(T)}catch(T){m(T instanceof Error?T.message:String(T))}finally{A(!1)}}};return e("dialog",{ref:y,id:"interface-dialog","aria-labelledby":"interface-dialog-title",onClose:a,onClick:p=>{p.target===p.currentTarget&&S()},onCancel:p=>{h&&p.preventDefault()},children:e("form",{ref:L,novalidate:!0,onSubmit:p=>{N(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"VPP vhost-user"}),e("h2",{id:"interface-dialog-title",children:"Add virtual interface"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:S,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:P,type:"text",required:!0,maxlength:64,pattern:Pi,autocomplete:"off",spellcheck:!1,placeholder:"vm01-eth0",value:s,onInput:p=>o(p.currentTarget.value)}),e("span",{class:"field-hint",children:"Creates a vhost-user socket under the configured VPP directory."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Rookery Network"}),e("select",{required:!0,value:f,onChange:p=>r(p.currentTarget.value),children:t.map(p=>e("option",{value:String(p.id),children:[p.name," \xB7 bd ",p.bridge_domain_id]},p.id))})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Attach to VM ",e("em",{children:"optional"})]}),e("select",{value:b,onChange:p=>w(p.currentTarget.value),children:[e("option",{value:"",children:"Leave unattached"}),n.map(p=>{let T=p.status==="running"||p.status==="starting";return e("option",{value:p.id,children:T?`${p.name} (running \u2014 stop first)`:p.name},p.id)})]}),e("span",{class:"field-hint",children:"Stop the VM before attaching or detaching interfaces."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:d,onChange:C,disabled:h})})]}),I?e("p",{class:"form-error",role:"alert",children:I}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:h,onClick:S,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:h,children:h?"Creating\u2026":"Add interface"})]})]})})}function us({iface:t,tables:n,onClose:a,onSaved:l}){let[s,o]=c(String(t.ipv4_table_id??0)),[f,r]=c(String(t.ipv6_table_id??0)),[b,w]=c(!1),[d,C]=c(""),h=ne(null);G(()=>{h.current?.showModal()},[]);let A=()=>{b||h.current?.close()},I=async y=>{if(y.preventDefault(),!b){C(""),w(!0);try{let P=await V(`/api/interfaces/${t.id}/route-tables`,{method:"PUT",body:JSON.stringify({ipv4_table_id:Number(s),ipv6_table_id:Number(f)})});l(P)}catch(P){C(P instanceof Error?P.message:String(P))}finally{w(!1)}}},m=y=>n.filter(P=>P.ipv6===y&&P.table_id!==0);return e("dialog",{ref:h,id:"interface-route-tables-dialog","aria-labelledby":"interface-route-tables-title",onClose:a,onClick:y=>{y.target===y.currentTarget&&A()},onCancel:y=>{b&&y.preventDefault()},children:e("form",{novalidate:!0,onSubmit:y=>{I(y)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Interface FIB binding"}),e("h2",{id:"interface-route-tables-title",children:["Route tables \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:A,children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.network_name," \xB7 ",t.mac,". Table 0 is the default VRF."]}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv4 FIB"}),e("select",{value:s,onChange:y=>o(y.currentTarget.value),children:[e("option",{value:"0",children:"default (0)"}),m(!1).map(y=>e("option",{value:String(y.table_id),children:[y.name," (",y.table_id,")"]},`v4-${y.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv6 FIB"}),e("select",{value:f,onChange:y=>r(y.currentTarget.value),children:[e("option",{value:"0",children:"default (0)"}),m(!0).map(y=>e("option",{value:String(y.table_id),children:[y.name," (",y.table_id,")"]},`v6-${y.id}`))]})]})]}),d?e("p",{class:"form-error",role:"alert",children:d}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:b,onClick:A,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:b,children:b?"Applying\u2026":"Apply route tables"})]})]})})}var Ei="[A-Za-z0-9][A-Za-z0-9._\\-]*";function ps({network:t,onClose:n,onSaved:a}){let l=!!t,[s,o]=c(t?.name||""),[f,r]=c(t?.ipv4_cidr||"10.0.0.0/16"),[b,w]=c(t?.ipv6_cidr||"fd00:20::/64"),[d,C]=ke("network",t?.id),[h,A]=c(!1),[I,m]=c(""),y=ne(null),P=ne(null),L=ne(null);G(()=>{y.current?.showModal(),P.current?.focus()},[]);let S=()=>{h||y.current?.close()},N=async p=>{if(p.preventDefault(),!(h||!L.current?.reportValidity())){m(""),A(!0);try{let T=await V(l&&t?`/api/networks/${t.id}`:"/api/networks",{method:l?"PUT":"POST",body:JSON.stringify(l?{ipv4_cidr:f.trim(),ipv6_cidr:b.trim()}:{name:s.trim(),ipv4_cidr:f.trim(),ipv6_cidr:b.trim()})});await Ce("network",T.id,T.name,d),a(T)}catch(T){m(T instanceof Error?T.message:String(T))}finally{A(!1)}}};return e("dialog",{ref:y,id:"network-dialog","aria-labelledby":"network-dialog-title",onClose:n,onClick:p=>{p.target===p.currentTarget&&S()},onCancel:p=>{h&&p.preventDefault()},children:e("form",{ref:L,novalidate:!0,onSubmit:p=>{N(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"VPP bridge domain"}),e("h2",{id:"network-dialog-title",children:l?"Edit Rookery Network":"Create Rookery Network"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:S,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:P,type:"text",required:!0,maxlength:64,pattern:Ei,autocomplete:"off",spellcheck:!1,placeholder:"lab-net",value:s,disabled:l,onInput:p=>o(p.currentTarget.value)}),e("span",{class:"field-hint",children:l?"The network name is immutable.":"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv4 network CIDR"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"10.0.0.0/16",value:f,onInput:p=>r(p.currentTarget.value)}),e("span",{class:"field-hint",children:["Address range for this network; .1 is the BVI and .2 the VNF stack.",l?" CIDRs can only be changed while the network has no VNFs, host taps, or BVI.":""]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv6 network CIDR"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"fd00:20::/64",value:b,onInput:p=>w(p.currentTarget.value)}),e("span",{class:"field-hint",children:"IPv6 range reserved for this network."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:d,onChange:C,disabled:h})})]}),I?e("p",{class:"form-error",role:"alert",children:I}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:h,onClick:S,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:h,children:h?"Creating\u2026":"Create network"})]})]})})}function fs({networks:t,onClose:n,onSaved:a}){let[l,s]=c(t[0]?String(t[0].id):""),[o,f]=c(t[1]?String(t[1].id):""),[r,b]=ke("network_pipe"),[w,d]=c(!1),[C,h]=c(""),A=ne(null),I=ne(null);G(()=>{A.current?.showModal()},[]);let m=()=>{w||A.current?.close()},y=async P=>{if(P.preventDefault(),!(w||!I.current?.reportValidity())){if(!l||!o||l===o){h("Choose two different networks.");return}d(!0),h("");try{let L=await V("/api/network-pipes",{method:"POST",body:JSON.stringify({network_a_id:Number(l),network_b_id:Number(o)})});await Ce("network_pipe",L.id,`pipes for ${L.network_a_name} and ${L.network_b_name}`,r),a(L)}catch(L){h(L instanceof Error?L.message:String(L))}finally{d(!1)}}};return e("dialog",{ref:A,id:"network-pipe-dialog","aria-labelledby":"network-pipe-title",onClose:n,onClick:P=>{P.target===P.currentTarget&&m()},onCancel:P=>{w&&P.preventDefault()},children:e("form",{ref:I,novalidate:!0,onSubmit:P=>{y(P)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Bridge-domain pipe"}),e("h2",{id:"network-pipe-title",children:"Connect two networks"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:m,children:"\xD7"})]}),e("p",{class:"field-hint",children:"Creates a VPP pipe pair and places one end in each bridge domain. This bridges L2 traffic between the two Rookery Networks."}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Network A"}),e("select",{required:!0,value:l,onChange:P=>s(P.currentTarget.value),children:t.map(P=>e("option",{value:String(P.id),children:P.name},P.id))})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Network B"}),e("select",{required:!0,value:o,onChange:P=>f(P.currentTarget.value),children:t.map(P=>e("option",{value:String(P.id),children:P.name},P.id))})]}),e("div",{class:"field field-wide",children:e(Se,{labels:r,onChange:b,disabled:w})})]}),t.length<2?e("p",{class:"field-hint",children:"Create at least two networks before adding a pipe."}):null,C?e("p",{class:"form-error",role:"alert",children:C}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:w,onClick:m,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:w||t.length<2,children:w?"Creating\u2026":"Create pipe"})]})]})})}var Li={interface:"any",direction:"both",maxMB:10,snapLength:2048};function Ht(t){return t===void 0||!Number.isFinite(t)||t<0?"\u2014":t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KiB`:t<1024*1024*1024?`${(t/(1024*1024)).toFixed(1)} MiB`:`${(t/(1024*1024*1024)).toFixed(2)} GiB`}function _n(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?t:n.toLocaleString()}function Ii(t){switch(t){case"rx":return"RX only";case"tx":return"TX only";case"both":case"rx/tx":return"RX + TX";default:return t||"\u2014"}}function ms(t){return`/api/troubleshoot/capture/files/${encodeURIComponent(t)}`}function hs(){let[t,n]=c([]),[a,l]=c(null),[s,o]=c([]),[f,r]=c(Li),[b,w]=c(!0),[d,C]=c(!1),[h,A]=c(!1),[I,m]=c(""),[y,P]=c(""),L=Math.min(1e6,Math.max(1,Math.ceil(f.maxMB*1024*1024/(f.snapLength+16)))),S=L*(f.snapLength+16),N=f.maxMB>=1&&f.maxMB<=100&&f.snapLength>=64&&f.snapLength<=9e3,p=!!a?.active,T=async()=>{let x=await V("/api/troubleshoot/capture/files"),R=Array.isArray(x)?x:[];o(R)},B=async()=>{w(!0),P("");let[x,R]=await Promise.allSettled([V("/api/troubleshoot/capture/interfaces"),V("/api/troubleshoot/capture")]);if(x.status==="fulfilled"?n(Array.isArray(x.value)?x.value:[]):P(x.reason instanceof Error?x.reason.message:String(x.reason)),R.status==="fulfilled"){let F=R.value;l(F.status??null),o(Array.isArray(F.files)?F.files:[])}else{y||P(R.reason instanceof Error?R.reason.message:String(R.reason));try{await T()}catch{}}w(!1)};G(()=>{B()},[]),G(()=>{if(!p)return;let x=!1,R=window.setInterval(async()=>{try{let F=await V("/api/troubleshoot/capture");if(x)return;let W=Array.isArray(F.files)?F.files:[];l(F.status??null),o(W),P("")}catch(F){x||P(F instanceof Error?F.message:String(F))}},1500);return()=>{x=!0,window.clearInterval(R)}},[p]);let K=async()=>{if(N){C(!0),P("");try{let x=await V("/api/troubleshoot/capture",{method:"POST",body:JSON.stringify({interface:f.interface,direction:f.direction,max_bytes:Math.round(f.maxMB*1024*1024),snap_length:f.snapLength})});l(x),ae("Packet capture started")}catch(x){let R=x instanceof Error?x.message:String(x);P(R),ae(R,!0)}finally{C(!1)}}},Z=async x=>{if(!(p&&a?.file===x.name)&&window.confirm(`Delete ${x.name}?`)){m(x.name),P("");try{await V(ms(x.name),{method:"DELETE"}),o(R=>R.filter(F=>F.name!==x.name)),l(R=>R&&R.file===x.name?{...R,file:""}:R),ae("Packet capture deleted")}catch(R){let F=R instanceof Error?R.message:String(R);P(F),ae(F,!0)}finally{m("")}}},ie=async()=>{A(!0),P("");try{let x=await V("/api/troubleshoot/capture/stop",{method:"POST"});l(x);try{await T()}catch{}ae("Packet capture stopped")}catch(x){let R=x instanceof Error?x.message:String(x);P(R),ae(R,!0)}finally{A(!1)}};return e("div",{class:"capture-panel",children:[e("div",{class:"capture-toolbar",children:[e("span",{class:p?"capture-badge active":"capture-badge",children:p?"Capturing":"Idle"}),e("button",{class:"button ghost",type:"button",disabled:b||p||d||h,onClick:()=>{B()},children:"Refresh"})]}),y?e("div",{class:"banner",role:"alert",children:y}):null,e("div",{class:"capture-form",children:[e("label",{children:[e("span",{children:"Interface"}),e("select",{value:f.interface,disabled:p,onChange:x=>r(R=>({...R,interface:x.currentTarget.value})),children:[e("option",{value:"any",children:"Any interface"}),t.map(x=>e("option",{value:x.name,children:[x.name," \xB7 #",x.sw_if_index,x.type?` \xB7 ${x.type}`:"",x.up?"":" \xB7 down"]},x.name))]})]}),e("label",{children:[e("span",{children:"Direction"}),e("select",{value:f.direction,disabled:p,onChange:x=>r(R=>({...R,direction:x.currentTarget.value})),children:[e("option",{value:"both",children:"RX + TX"}),e("option",{value:"rx",children:"RX only"}),e("option",{value:"tx",children:"TX only"})]})]}),e("label",{children:[e("span",{children:"Max size (MB)"}),e("input",{type:"number",min:1,max:100,step:1,value:f.maxMB,disabled:p,onChange:x=>r(R=>({...R,maxMB:Number(x.currentTarget.value)}))})]}),e("label",{children:[e("span",{children:"Snap length (bytes)"}),e("input",{type:"number",min:64,max:9e3,step:64,value:f.snapLength,disabled:p,onChange:x=>r(R=>({...R,snapLength:Number(x.currentTarget.value)}))})]})]}),e("p",{class:"field-hint",children:["Approx. ",Ht(S)," /"," ",L.toLocaleString()," packets. VPP keeps up to the computed packet count and writes the pcap under the host\u2019s ",e("code",{children:"/tmp"}),"; only traffic on the selected interface is captured."]}),e("div",{class:"capture-actions",children:[p?e("button",{class:"button primary",type:"button",disabled:h,onClick:()=>{ie()},children:h?"Stopping\u2026":"Stop capture"}):e("button",{class:"button primary",type:"button",disabled:!N||d||b,onClick:()=>{K()},children:d?"Starting\u2026":"Start capture"}),a?.file?e("span",{class:"capture-current-file",children:[p?"Writing":"Last capture",": ",e("code",{children:a.file})]}):null]}),a?e("div",{class:p?"capture-status active":"capture-status",children:[e("div",{class:"capture-status-head",children:[e("strong",{children:p?"Capture running":"No capture running"}),a.interface?e("span",{children:a.interface}):null]}),e("div",{class:"capture-status-grid",children:[e("span",{children:[e("b",{children:"Direction"}),Ii(a.direction)]}),e("span",{children:[e("b",{children:"Size"}),Ht(a.size)]}),e("span",{children:[e("b",{children:p?"Buffer":"Captured"}),p?`${Ye(a.buffered)} / ${Ye(a.max_packets)} packets`:`${Ye(a.packets)} packets`]}),e("span",{children:[e("b",{children:"Limit"}),Ye(a.max_packets)," packets \xB7 ",Ht(a.max_bytes)]}),e("span",{children:[e("b",{children:"Started"}),_n(a.started_at)]}),a.stopped_at?e("span",{children:[e("b",{children:"Stopped"}),_n(a.stopped_at)]}):null]}),a.raw?e("details",{class:"capture-raw",children:[e("summary",{children:"VPP output"}),e("pre",{children:a.raw})]}):null]}):null,e("div",{class:"capture-files",children:[e("div",{class:"capture-files-head",children:[e("h3",{children:"Captures"}),e("span",{children:[s.length," file",s.length===1?"":"s"]})]}),s.length===0?e("p",{class:"field-hint",children:"No captures yet. Start a capture, then stop it to inspect the pcap."}):e("div",{class:"table-wrap",children:e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{children:"File"}),e("th",{children:"Size"}),e("th",{children:"Modified"}),e("th",{children:"Actions"})]})}),e("tbody",{children:s.map(x=>e("tr",{children:[e("td",{children:e("code",{children:x.name})}),e("td",{children:Ht(x.size)}),e("td",{children:_n(x.modified)}),e("td",{children:e("div",{class:"capture-file-actions",children:[e("a",{class:"button ghost",href:ms(x.name),download:!0,children:"Download"}),e("button",{class:"button ghost danger",type:"button",disabled:I===x.name||p&&a?.file===x.name,title:p&&a?.file===x.name?"Stop the capture before deleting it":void 0,onClick:()=>{Z(x)},children:I===x.name?"Deleting\u2026":"Delete"})]})})]},x.name))})]})})]})]})}var Ai=/^\s*Packet\s+(\d+)\s*$/,bs=/^\s*\d{2}:\d{2}:\d{2}:\d+:\s+(\S+)/;function Ni(t){let n=t.replace(/\r/g,"").split(` +`),a=[],l=null;for(let s of n){let o=s.match(Ai);if(o){l&&a.push({...l,raw:l.lines.join(` +`).trim()}),l={number:Number(o[1]),nodes:[],lines:[s]};continue}if(!l)continue;l.lines.push(s);let f=s.match(bs);f&&l.nodes.push(f[1])}if(l&&a.push({...l,raw:l.lines.join(` +`).trim()}),a.length===0){let s=n.map(o=>o.match(bs)?.[1]).filter(o=>!!o);s.length>0&&a.push({number:1,nodes:s,raw:t.trim()})}return a}function Di({packet:t}){return t.nodes.length===0?e("p",{class:"empty-state",children:"No VPP node path found for this packet."}):e("div",{class:"trace-path-panel",children:[e("div",{class:"trace-path-head",children:[e("strong",{children:["Packet ",t.number]}),e("span",{class:"muted",children:[t.nodes.length," nodes"]})]}),e("ol",{class:"trace-path","aria-label":`Packet ${t.number} node path`,children:t.nodes.map((n,a)=>e("li",{children:[e("span",{class:"trace-path-node","data-drop":n.includes("drop")?"true":void 0,children:n}),a{L(!0);try{let F=await V("/api/troubleshoot/trace/nodes"),W=Array.isArray(F)?F:Array.isArray(F.nodes)?F.nodes:[];y(W),N("")}catch(F){N(F instanceof Error?F.message:String(F))}finally{L(!1)}};G(()=>{t&&B()},[t]),G(()=>{n||m.length===0||a(m[0])},[n,m]);let K=async F=>{if(F.preventDefault(),!(r||!n.trim())){I(""),b(!0),f(null);try{let W=await V("/api/troubleshoot/trace",{method:"POST",body:JSON.stringify({node:n.trim(),count:Number(l)})});f({...W,raw:""})}catch(W){I(W instanceof Error?W.message:String(W))}finally{b(!1)}}},Z=async()=>{I(""),d(!0);try{let F=await V("/api/troubleshoot/trace");f(W=>({...F,node:W?.node??n,count:W?.count??Number(l)}))}catch(F){I(F instanceof Error?F.message:String(F))}finally{d(!1)}},ie=We(()=>Ni(o?.raw??""),[o?.raw]);G(()=>{T(0)},[o?.raw]);let x=ie[p],R=async()=>{I(""),h(!0);try{await V("/api/troubleshoot/trace",{method:"DELETE"}),f(null)}catch(F){I(F instanceof Error?F.message:String(F))}finally{h(!1)}};return e("div",{class:"trace-panel",children:[e("form",{class:"trace-form",novalidate:!0,onSubmit:F=>{K(F)},children:[e("div",{class:"field trace-node-field",children:[e("span",{class:"field-label",children:"VPP graph node"}),e("select",{class:"trace-node-list",required:!0,value:n,onChange:F=>a(F.currentTarget.value),children:[e("option",{value:"",disabled:!0,children:"Select a VPP node\u2026"}),m.map(F=>e("option",{value:F,children:F},F))]}),e("span",{class:"field-hint",children:m.length>0?`${m.length} VPP node${m.length===1?"":"s"} available.`:S||"No VPP nodes loaded yet."}),e("button",{class:"row-action trace-refresh-nodes",type:"button",disabled:P,onClick:()=>{B()},children:P?"Loading nodes\u2026":"Refresh nodes"})]}),e("label",{class:"field trace-count-field",children:[e("span",{class:"field-label",children:"Packet count"}),e("input",{type:"number",required:!0,min:1,max:1e3,value:l,onInput:F=>s(F.currentTarget.value)}),e("span",{class:"field-hint",children:"Trace up to 1,000 packets."})]}),e("button",{class:"button primary",type:"submit",disabled:r||!n.trim()||Number(l)<1||Number(l)>1e3,children:r?"Starting\u2026":"Start trace"}),e("button",{class:"button ghost",type:"button",disabled:w,onClick:()=>{Z()},children:w?"Loading\u2026":"Show trace"}),e("button",{class:"button ghost",type:"button",disabled:C,onClick:()=>{R()},children:C?"Clearing\u2026":"Clear"})]}),e("p",{class:"field-hint",children:["Start a trace, generate traffic through the selected node, then click ",e("strong",{children:"Show trace"}),"."]}),A?e("div",{class:"banner",role:"alert",children:A}):null,o?e("div",{class:"trace-result",children:[e("div",{class:"trace-result-head",children:[e("span",{children:o.node?`${o.node} \xB7 ${o.count||0} packets`:"VPP trace"}),o.captured_at?e("span",{class:"muted",children:new Date(o.captured_at).toLocaleString()}):null]}),ie.length>0?e("div",{class:"trace-visual",children:[e("label",{class:"field trace-packet-field",children:[e("span",{class:"field-label",children:"Packet"}),e("select",{value:String(p),onChange:F=>T(Number(F.currentTarget.value)),children:ie.map((F,W)=>e("option",{value:String(W),children:["Packet ",F.number," \xB7 ",F.nodes.length," nodes"]},`${F.number}-${W}`))}),e("span",{class:"field-hint",children:"Select a packet to draw its VPP node path."})]}),x?e(Di,{packet:x}):null]}):e("p",{class:"empty-state",children:"No trace packets captured yet. Generate traffic and click Show trace."}),e("details",{class:"trace-raw",children:[e("summary",{children:"Raw trace text"}),e("pre",{class:"trace-output",children:o.raw||"No trace packets captured yet. Generate traffic and click Show trace."})]})]}):e("p",{class:"empty-state",children:"No trace loaded."})]})}function vs({table:t,routeID:n,interfaces:a,bviInterfaces:l,hostInterfaces:s,hostTaps:o,tables:f,onBack:r,onSaved:b}){let w=n!==void 0,[d,C]=c(w),[h,A]=c(!1),[I,m]=c(""),[y,P]=c(t.ipv6?"2001:db8::/64":"10.0.0.0/24"),[L,S]=c(""),[N,p]=c(t.ipv6?"2001:db8::1":"10.0.0.1"),[T,B]=c(""),[K,Z]=c(""),[ie,x]=c(""),[R,F]=c(""),[W,Q]=c(""),[g,oe]=c(""),[E,z]=c(""),[re,ue]=c(""),[v,M]=c(""),[k,O]=c(""),[le,se]=c(!1),[fe,ee]=c(!1),[de,me]=c(""),[u,_]=c(""),[$,j]=c(""),[J,pe]=c(!1),[Le,q]=ke("route",n);G(()=>{document.title=`${w?"Edit":"Add"} route \xB7 ${t.name} \xB7 rookery`},[w,t.name]),G(()=>{if(n===void 0)return;let X=!1;return C(!0),V(`/api/route-tables/${t.id}/routes?ipv6=${t.ipv6}`).then($e=>{if(X)return;let H=(Array.isArray($e)?$e:[]).find(_e=>_e.id===n);if(!H){m("Route not found.");return}P(H.prefix),S(H.description||""),p(H.next_hop||""),H.interface_id?(B(String(H.interface_id)),Z("")):(B(""),Z(H.egress_interface||"")),x(H.preference===void 0?"":String(H.preference)),F(H.count===void 0?"":String(H.count)),Q(H.next_hop_table===void 0?"":String(H.next_hop_table)),oe(H.weight===void 0?"":String(H.weight)),z(H.udp_encap===void 0?"":String(H.udp_encap)),ue(H.ipv4_lookup_table===void 0?"":String(H.ipv4_lookup_table)),M(H.ipv6_lookup_table===void 0?"":String(H.ipv6_lookup_table)),O(H.mpls_lookup_table===void 0?"":String(H.mpls_lookup_table)),se(!!H.resolve_via_host),ee(!!H.resolve_via_connected),me(H.rx_ipv4_interface||""),_(H.rx_ipv6_interface||""),j((H.out_labels||[]).join(" ")),pe(!!H.drop),m("")}).catch($e=>m($e instanceof Error?$e.message:String($e))).finally(()=>{X||C(!1)}),()=>{X=!0}},[n,t.id,t.ipv6]);let he=async X=>{if(X.preventDefault(),h||d)return;if(!y.trim()){m("Enter a destination prefix.");return}if(de.trim()&&u.trim()){m("rx-ip4 and rx-ip6 are mutually exclusive.");return}if(!(N.trim()||T||K.trim()||J||de.trim()||u.trim()||W||E||re||v||k)){m("Enter a next hop, egress interface, rx interface, lookup option, or drop.");return}let H=T.startsWith("vpp:")?T.slice(4):"",_e=K.trim()||H,Ee=T&&!H&&!K.trim()?Number(T):0,U=$.split(/[\s,]+/).map(Ae=>Ae.trim()).filter(Boolean).map(Ae=>Math.trunc(Number(Ae))).filter(Ae=>Number.isFinite(Ae)&&Ae>=0),we=Ae=>{let ft=Ae.trim();if(!ft)return;let it=Math.max(0,Math.trunc(Number(ft)));return Number.isFinite(it)?it:void 0},Te=we(ie),xe=we(R),Oe=we(g),et=we(E),Ke=we(k),ct=W.trim(),Ve=re.trim(),Ie=v.trim(),je={ipv6:t.ipv6,prefix:y.trim(),description:L.trim(),next_hop:N.trim(),...Ee?{interface_id:Ee}:{},..._e?{egress_interface:_e}:{},...Te!==void 0?{preference:Te}:{},...xe!==void 0?{count:xe}:{},...ct?{next_hop_table:ct}:{},...Oe!==void 0?{weight:Oe}:{},...et!==void 0?{udp_encap:et}:{},...Ve?{ipv4_lookup_table:Ve}:{},...Ie?{ipv6_lookup_table:Ie}:{},...Ke!==void 0?{mpls_lookup_table:Ke}:{},resolve_via_host:le,resolve_via_connected:fe,rx_ipv4_interface:de.trim(),rx_ipv6_interface:u.trim(),out_labels:U,drop:J};A(!0),m("");try{let Ae;if(n===void 0)Ae=await V(`/api/route-tables/${t.id}/routes`,{method:"POST",body:JSON.stringify(je)});else{let{ipv6:ft,...it}=je;Ae=await V(`/api/routes/${n}`,{method:"PUT",body:JSON.stringify({id:n,...it})})}await Ce("route",Ae.id,Ae.prefix,Le),b()}catch(Ae){m(Ae instanceof Error?Ae.message:String(Ae))}finally{A(!1)}},ve=X=>f.filter($e=>$e.ipv6===X&&$e.table_id!==0);return e("div",{class:"route-page",children:[e("header",{class:"route-page-head",children:e("div",{children:[e("button",{class:"back-link",type:"button",onClick:r,children:["\u2190 Routes \xB7 ",t.name]}),e("p",{class:"eyebrow",children:[t.ipv6?"IPv6":"IPv4"," FIB \xB7 table ",t.table_id]}),e("h2",{children:w?"Edit route":"Add route"}),e("p",{class:"panel-subtitle",children:["Route options map directly to the VPP ",e("code",{children:"ip route"})," command."]})]})}),e("form",{class:"route-page-form",onSubmit:X=>{he(X)},children:[d?e("p",{class:"field-hint",children:"Loading route\u2026"}):null,e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Prefix"}),e("input",{type:"text",required:!0,value:y,onInput:X=>P(X.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Next hop"}),e("input",{type:"text",value:N,onInput:X=>p(X.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Egress interface"}),e("select",{value:T,onChange:X=>B(X.currentTarget.value),children:[e("option",{value:"",children:"Resolve via table"}),a.map(X=>e("option",{value:String(X.id),children:X.name},X.id)),l.length>0?e("optgroup",{label:"Network gateways (BVI)",children:l.map(X=>e("option",{value:`vpp:${X}`,children:X},`bvi-${X}`))}):null,s.some(X=>X.in_vpp&&X.vpp_name)?e("optgroup",{label:"Host interfaces",children:s.filter(X=>X.in_vpp&&X.vpp_name).map(X=>e("option",{value:`vpp:${X.vpp_name}`,children:[X.vpp_name," (host)"]},`host-${X.name}`))}):null,o.length>0?e("optgroup",{label:"Host taps",children:o.map(X=>e("option",{value:`vpp:${X.name}`,children:X.name},`tap-${X.id}`))}):null]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Other VPP interface"}),e("input",{type:"text",placeholder:"host-eno4",value:K,onInput:X=>Z(X.currentTarget.value)}),e("span",{class:"field-hint",children:"Overrides the picker when set."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Metric / preference"}),e("input",{type:"number",min:"0",max:"255",value:ie,onInput:X=>x(X.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Description"}),e("textarea",{rows:2,maxlength:1e3,value:L,onInput:X=>S(X.currentTarget.value)})]}),e("div",{class:"field field-wide",children:e(Se,{labels:Le,onChange:q,disabled:h})})]}),e("section",{class:"route-advanced","aria-labelledby":"route-advanced-title",children:[e("h4",{class:"route-advanced-title",id:"route-advanced-title",children:"Advanced VPP route options"}),e("p",{class:"field-hint",children:["Leave an option blank to omit it from the VPP command. Next-hop and IP lookup tables use route-table names; choose ",e("code",{children:"default"}),"for table ",e("code",{children:"0"}),". MPLS lookup uses a numeric table ID."]}),e("div",{class:"route-advanced-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Count"}),e("input",{type:"number",min:"1",max:"100000",placeholder:"1",value:R,onInput:X=>F(X.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Next-hop table"}),e("select",{value:W,onChange:X=>Q(X.currentTarget.value),children:[e("option",{value:"",children:"Omit"}),e("option",{value:"default",children:"default (0)"}),ve(t.ipv6).map(X=>e("option",{value:X.name,children:[X.name," (",X.table_id,")"]},`nh-${X.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Weight"}),e("input",{type:"number",min:"0",max:"255",value:g,onInput:X=>oe(X.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"UDP encap"}),e("input",{type:"number",min:"0",value:E,onInput:X=>z(X.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv4 lookup table"}),e("select",{value:re,onChange:X=>ue(X.currentTarget.value),children:[e("option",{value:"",children:"Omit"}),e("option",{value:"default",children:"default (0)"}),ve(!1).map(X=>e("option",{value:X.name,children:[X.name," (",X.table_id,")"]},`v4-${X.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv6 lookup table"}),e("select",{value:v,onChange:X=>M(X.currentTarget.value),children:[e("option",{value:"",children:"Omit"}),e("option",{value:"default",children:"default (0)"}),ve(!0).map(X=>e("option",{value:X.name,children:[X.name," (",X.table_id,")"]},`v6-${X.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"MPLS lookup table ID"}),e("input",{type:"number",min:"0",value:k,onInput:X=>O(X.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"rx-ip4 interface"}),e("input",{type:"text",value:de,onInput:X=>me(X.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"rx-ip6 interface"}),e("input",{type:"text",value:u,onInput:X=>_(X.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Out labels"}),e("input",{type:"text",placeholder:"100 200 300",value:$,onInput:X=>j(X.currentTarget.value)})]}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:le,onChange:X=>se(X.currentTarget.checked)}),e("span",{children:"resolve-via-host"})]}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:fe,onChange:X=>ee(X.currentTarget.checked)}),e("span",{children:"resolve-via-connected"})]}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:J,onChange:X=>pe(X.currentTarget.checked)}),e("span",{children:"drop"})]})]})]}),I?e("p",{class:"form-error",role:"alert",children:I}):null,e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",onClick:r,disabled:h,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:h||d,children:h?"Saving\u2026":w?"Save route":"Add route"})]})]})]})}function ys({tables:t,onCancel:n,onSaved:a}){let l=Math.max(0,...t.filter(P=>!P.is_default).map(P=>P.table_id))+1,[s,o]=c(""),[f,r]=c(String(l)),[b,w]=c(!1),[d,C]=ke("route_table"),[h,A]=c(!1),[I,m]=c("");G(()=>{document.title="Create route table \xB7 rookery"},[]);let y=async P=>{if(P.preventDefault(),!h){m(""),A(!0);try{let L=await V("/api/route-tables",{method:"POST",body:JSON.stringify({name:s.trim(),table_id:Number(f),ipv6:b})});await Ce("route_table",L.id,L.name,d),a(L)}catch(L){m(L instanceof Error?L.message:String(L))}finally{A(!1)}}};return e("div",{class:"route-page",children:[e("header",{class:"route-page-head",children:e("div",{children:[e("button",{class:"back-link",type:"button",onClick:n,children:"\u2190 Route tables"}),e("p",{class:"eyebrow",children:"IP FIB / VRF"}),e("h2",{children:"Create route table"}),e("p",{class:"panel-subtitle",children:"Define an IPv4 or IPv6 VRF/FIB table. Table 0 is the built-in default VRF."})]})}),e("form",{class:"route-page-form",novalidate:!0,onSubmit:P=>{y(P)},children:[e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Name"}),e("input",{type:"text",required:!0,maxLength:63,placeholder:"tenant-blue",value:s,onInput:P=>o(P.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Table ID"}),e("input",{type:"number",required:!0,min:1,value:f,onInput:P=>r(P.currentTarget.value)}),e("span",{class:"field-hint",children:"Custom tables start at 1; table 0 is the default VRF."})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:b,onChange:P=>w(P.currentTarget.checked)}),e("span",{children:"IPv6 FIB table"})]}),e("span",{class:"field-hint",children:"The same table ID can be used separately for IPv4 and IPv6."})]}),e("div",{class:"field field-wide",children:e(Se,{labels:d,onChange:C,disabled:h})})]}),I?e("p",{class:"form-error",role:"alert",children:I}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:h,onClick:n,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:h||!s.trim()||Number(f)<1,children:h?"Creating\u2026":"Create route table"})]})]})]})}function xi(t){let n=[];return(t.count||0)>1&&n.push(`count ${t.count}`),t.next_hop_table&&n.push(`next-hop-table ${t.next_hop_table}`),t.weight&&n.push(`weight ${t.weight}`),t.udp_encap&&n.push(`udp-encap ${t.udp_encap}`),t.ipv4_lookup_table&&n.push(`ip4-lookup-in-table ${t.ipv4_lookup_table}`),t.ipv6_lookup_table&&n.push(`ip6-lookup-in-table ${t.ipv6_lookup_table}`),t.mpls_lookup_table&&n.push(`mpls-lookup-in-table ${t.mpls_lookup_table}`),t.resolve_via_host&&n.push("resolve-via-host"),t.resolve_via_connected&&n.push("resolve-via-connected"),t.rx_ipv4_interface&&n.push(`rx-ip4 ${t.rx_ipv4_interface}`),t.rx_ipv6_interface&&n.push(`rx-ip6 ${t.rx_ipv6_interface}`),t.out_labels&&t.out_labels.length>0&&n.push(`out-labels ${t.out_labels.join(" ")}`),t.drop&&n.push("drop"),n.join(" \xB7 ")}function _s({table:t,onBack:n,onChanged:a}){let[l,s]=c([]),[o,f]=c([]),[r,b]=c(!0),[w,d]=c(!0),[C,h]=c(""),[A,I]=c(""),m=Ne("route"),y=`/route-tables/${t.id}/routes/new?ipv6=${t.ipv6}`,P=async()=>{b(!0);try{let p=await V(`/api/route-tables/${t.id}/routes?ipv6=${t.ipv6}`);s(Array.isArray(p)?p:[]),h("")}catch(p){h(p instanceof Error?p.message:String(p))}finally{b(!1)}},L=async()=>{d(!0);try{let p=await V(`/api/route-tables/${t.id}/fib?ipv6=${t.ipv6}`);f(Array.isArray(p)?p:[]),I("")}catch(p){I(p instanceof Error?p.message:String(p))}finally{d(!1)}};G(()=>{P()},[t.id,t.ipv6]),G(()=>{L()},[t.id,t.ipv6]),G(()=>{document.title=`Routes \xB7 ${t.name} \xB7 rookery`},[t.name]);let S=async p=>{if(window.confirm(`Delete route ${p.prefix}${p.next_hop?` via ${p.next_hop}`:""}?`)){h("");try{await V(`/api/routes/${p.id}`,{method:"DELETE"}),s(T=>T.filter(B=>B.id!==p.id)),await L(),a()}catch(T){h(T instanceof Error?T.message:String(T))}}},N=o.filter(p=>p.dynamic);return e("div",{class:"route-page",children:[e("header",{class:"route-page-head",children:[e("div",{children:[e("button",{class:"back-link",type:"button",onClick:n,children:"\u2190 Route tables"}),e("p",{class:"eyebrow",children:[t.ipv6?"IPv6":"IPv4"," FIB \xB7 table ",t.table_id,t.is_default?" \xB7 default VRF":""]}),e("h2",{children:["Routes \xB7 ",t.name]}),e("p",{class:"panel-subtitle",children:"Static routes are managed by Rookery. Dynamic routes are read directly from the live VPP FIB."})]}),e("button",{class:"button primary",type:"button",onClick:()=>te(y),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add route"]})]}),e("section",{class:"route-page-section","aria-labelledby":"static-routes-title",children:[e("div",{class:"panel-head",children:e("div",{children:[e("h3",{id:"static-routes-title",children:"Static routes"}),e("p",{class:"field-hint",children:"Prefixes installed in this VRF by Rookery."})]})}),C?e("p",{class:"form-error",role:"alert",children:C}):null,e("div",{class:"table-wrap route-table-wrap",children:r?e("p",{class:"empty-state",children:"Loading routes\u2026"}):l.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Prefix"}),e("th",{scope:"col",children:"Next hop"}),e("th",{scope:"col",children:"Interface"}),e("th",{scope:"col",children:"Options"}),e("th",{scope:"col",children:"Preference"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:l.map(p=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name mono",children:p.prefix}),p.description?e("span",{class:"vm-description",children:p.description}):null,e(De,{entityType:"route",entityID:p.id,name:p.prefix,labels:m.labels[String(p.id)],onSaved:T=>m.setEntityLabels(String(p.id),T)})]}),e("td",{class:"mono",children:p.drop?"drop":p.next_hop||"attached"}),e("td",{class:"mono",children:p.egress_interface||p.interface_name||"resolved by table"}),e("td",{class:"mono",children:xi(p)||"\u2014"}),e("td",{class:"mono",children:p.preference||0}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>te(`/route-tables/${t.id}/routes/${p.id}/edit?ipv6=${t.ipv6}`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{S(p)},children:"Delete"})]})})]},p.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2197"}),e("h3",{children:"No static routes"}),e("p",{children:"Add a route to this VRF."}),e("button",{class:"button primary",type:"button",onClick:()=>te(y),children:"Add route"})]})})]}),e("section",{class:"fib-section route-page-section","aria-labelledby":"fib-routes-title",children:[e("div",{class:"panel-head fib-head",children:[e("div",{children:[e("h3",{id:"fib-routes-title",children:"Dynamic routes"}),e("p",{class:"field-hint",children:"Read-only entries from the live VPP FIB, including connected, DHCP, ND, and adjacency routes."})]}),e("button",{class:"button ghost",type:"button",disabled:w,onClick:()=>{L()},children:w?"Refreshing\u2026":"Refresh FIB"})]}),A?e("p",{class:"form-error",role:"alert",children:A}):w?e("p",{class:"empty-state",children:"Loading VPP FIB\u2026"}):N.length>0?e("div",{class:"table-wrap route-table-wrap",children:e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Prefix"}),e("th",{scope:"col",children:"Source"}),e("th",{scope:"col",children:"Paths"})]})}),e("tbody",{children:N.map(p=>e("tr",{children:[e("td",{class:"mono",children:p.prefix}),e("td",{children:e("span",{class:"fib-source",children:p.source_name})}),e("td",{children:(p.paths??[]).length>0?(p.paths??[]).map((T,B)=>e("div",{class:"fib-path",children:[e("span",{class:"fib-path-type",children:T.type})," ",e("span",{class:"mono",children:T.next_hop||"\u2014"}),T.interface_name?e("span",{children:[" via ",T.interface_name]}):null]},`${p.prefix}-${B}`)):e("span",{class:"muted",children:"no paths"})})]},`${p.ipv6?"v6":"v4"}-${p.prefix}`))})]})}):e("p",{class:"empty-state",children:"No dynamic routes in this table."})]})]})}function ws({name:t,label:n,hostAddresses:a,onClose:l}){let[s,o]=c(""),[f,r]=c(!1),[b,w]=c([]),[d,C]=c(!1),[h,A]=c(""),[I,m]=c(""),[y,P]=c(!0),[L,S]=c(!1),[N,p]=c(!1),[T,B]=c(""),K=ne(null),Z=ne(null),ie=`/api/vpp/interfaces/${encodeURIComponent(t)}/addresses`,x=g=>{w(g.live_addresses||[]),C(!!g.dhcp_client_configured),A(g.dhcp_client_status||""),m(g.status_detail||"")};G(()=>{K.current?.showModal(),R()},[]);let R=async()=>{P(!0);try{let g=await V(ie);o((g.addresses||[]).join(` +`)),r(!!g.dhcp_client_enabled),x(g),B("")}catch(g){B(g instanceof Error?g.message:String(g))}finally{P(!1),Z.current?.focus()}},F=async()=>{if(!L){S(!0);try{let g=await V(ie);x(g),B("")}catch(g){B(g instanceof Error?g.message:String(g))}finally{S(!1)}}},W=()=>{N||K.current?.close()},Q=async g=>{if(g.preventDefault(),N||y)return;p(!0),B("");let oe=s.split(/\r?\n/).map(E=>E.trim()).filter(Boolean);try{let E=await V(ie,{method:"PUT",body:JSON.stringify({addresses:oe,dhcp_client_enabled:f})});o((E.addresses||[]).join(` +`)),r(!!E.dhcp_client_enabled),x(E),ae(`Saved configuration for ${t}`)}catch(E){B(E instanceof Error?E.message:String(E))}finally{p(!1)}};return e("dialog",{ref:K,id:"vpp-addresses-dialog","aria-labelledby":"vpp-addresses-title",onClose:l,onClick:g=>{g.target===g.currentTarget&&W()},onCancel:g=>{N&&g.preventDefault()},children:e("form",{onSubmit:g=>{Q(g)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"VPP interface addresses"}),e("h2",{id:"vpp-addresses-title",children:["Addresses \xB7 ",t]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:W,children:"\xD7"})]}),e("div",{class:"startup-script-status",children:[e("span",{class:"status-badge","data-status":"running",children:n||"VPP interface"}),e("span",{class:"field-hint",children:"IPv4 and IPv6 CIDRs are applied directly to this VPP interface."})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:f,onChange:g=>r(g.currentTarget.checked)}),e("span",{children:"Enable DHCP client (IPv4)"})]}),e("span",{class:"field-hint",children:"VPP requests an IPv4 lease from the network. This can be combined with static addresses."})]}),a?e("div",{class:"vpp-address-live field-wide",children:[e("div",{class:"vpp-address-live-head",children:e("span",{class:"field-label",children:"Host side addresses"})}),a.length>0?e("div",{class:"vpp-live-addresses mono",children:a.map(g=>e("span",{children:g},g))}):e("p",{class:"field-hint",children:"No host-side address assigned."})]}):null,e("div",{class:"vpp-address-live field-wide",children:[e("div",{class:"vpp-address-live-head",children:[e("span",{class:"field-label",children:"VPP side addresses"}),e("div",{class:"actions",children:[e("span",{class:"status-badge","data-status":d?"running":"disabled",children:["DHCP client ",d?"configured":"not configured"]}),e("button",{class:"row-action",type:"button",disabled:L||N,onClick:()=>{F()},children:L?"Refreshing\u2026":"Refresh status"})]})]}),I?e("p",{class:"field-hint",children:I}):b.length>0?e("div",{class:"vpp-live-addresses mono",children:b.map(g=>e("span",{children:g},g))}):e("p",{class:"field-hint",children:["No live address assigned yet",f?"; waiting for a DHCP lease.":"."]}),h?e("details",{class:"startup-script-output",children:[e("summary",{children:"DHCP client status"}),e("pre",{children:h})]}):null]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Addresses"}),e("textarea",{ref:Z,class:"startup-script-editor",rows:8,spellcheck:!1,placeholder:`10.20.0.10/24 +fd00:20::10/64`,value:s,onInput:g=>o(g.currentTarget.value)}),e("span",{class:"field-hint",children:"One address per line, using CIDR notation. Leave empty to remove all Rookery-managed addresses. Existing addresses managed by other Rookery features are not shown here."})]}),T?e("p",{class:"form-error",role:"alert",children:T}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:N,onClick:W,children:"Close"}),e("button",{class:"button primary",type:"submit",disabled:N||y,children:N?"Saving\u2026":"Save addresses"})]})]})})}function ks({name:t,label:n,tables:a,onClose:l,onSaved:s}){let[o,f]=c("0"),[r,b]=c("0"),[w,d]=c(!0),[C,h]=c(!1),[A,I]=c(""),m=ne(null),y=`/api/vpp/interfaces/${encodeURIComponent(t)}/route-tables`;G(()=>{m.current?.showModal(),P()},[]);let P=async()=>{d(!0);try{let p=await V(y);f(String(p.ipv4_table_id??0)),b(String(p.ipv6_table_id??0)),I("")}catch(p){I(p instanceof Error?p.message:String(p))}finally{d(!1)}},L=()=>{C||m.current?.close()},S=async p=>{if(p.preventDefault(),!(w||C)){h(!0),I("");try{let T=await V(y,{method:"PUT",body:JSON.stringify({ipv4_table_id:Number(o),ipv6_table_id:Number(r)})});s(T)}catch(T){I(T instanceof Error?T.message:String(T))}finally{h(!1)}}},N=p=>a.filter(T=>T.ipv6===p&&T.table_id!==0);return e("dialog",{ref:m,id:"vpp-interface-route-tables-dialog","aria-labelledby":"vpp-interface-route-tables-title",onClose:l,onClick:p=>{p.target===p.currentTarget&&L()},onCancel:p=>{C&&p.preventDefault()},children:e("form",{novalidate:!0,onSubmit:p=>{S(p)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Interface FIB binding"}),e("h2",{id:"vpp-interface-route-tables-title",children:["Route tables \xB7 ",t]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:L,children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[n,". Table 0 is the default VRF. Saving an explicit binding, including default (0), overrides any automatic network VRF for a host tap and may affect reachability."]}),e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv4 FIB"}),e("select",{value:o,disabled:w||C,onChange:p=>f(p.currentTarget.value),children:[e("option",{value:"0",children:"default (0)"}),N(!1).map(p=>e("option",{value:String(p.table_id),children:[p.name," (",p.table_id,")"]},`v4-${p.id}`))]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"IPv6 FIB"}),e("select",{value:r,disabled:w||C,onChange:p=>b(p.currentTarget.value),children:[e("option",{value:"0",children:"default (0)"}),N(!0).map(p=>e("option",{value:String(p.table_id),children:[p.name," (",p.table_id,")"]},`v6-${p.id}`))]})]})]}),A?e("p",{class:"form-error",role:"alert",children:A}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:C,onClick:L,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:w||C,children:C?"Applying\u2026":"Apply route tables"})]})]})})}var kn=["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"],Cs=92,Ss=104,yt=60,Ts=900,Vi=220,Mi=/^(?:vhost-user-input|tap-input|af-packet-input|interface-output|interface-\d+-(?:output|tx)(?:-deleted)?|(?:bvi|host|vm|vnf)[^ ]*-(?:output|tx)(?:-deleted)?)$/;function $i(t){let n=/^(.*?)-(?:output|tx)(?:-deleted)?$/.exec(t);return n?n[1]:t}function Ri(t){return t==="vhost-user-input"?"vhost-user input":t==="tap-input"?"tap input":t==="af-packet-input"?"af-packet input":t==="interface"?"interface (generic)":/^interface-\d+$/.test(t)?`${t} (deleted)`:t.startsWith("vm-")?`${t} (VM interface)`:/^vnf/.test(t)?`${t} (VNF tap)`:/^bvi/.test(t)?`${t} (BVI)`:t.startsWith("host-")?`${t} (host NIC)`:t}function Fi(t){let n=new Map;for(let a of t.nodes){if(!a.isInterface)continue;let l=$i(a.id),s=n.get(l);s?s.push(a.id):n.set(l,[a.id])}return Array.from(n.entries()).map(([a,l])=>({id:a,label:Ri(a),nodeIDs:l})).sort((a,l)=>a.label.localeCompare(l.label))}function Bi(t){let n=[],a=(l,s,o)=>{let f=t.nodes.filter(r=>o(r.id)).map(r=>r.id);f.length>0&&n.push({id:l,label:s,nodeIDs:f})};return a("feature:acl","ACL plugin",l=>l.startsWith("acl-plugin")),a("feature:nat","NAT44",l=>l.startsWith("nat")),a("feature:dhcp","DHCP",l=>l.includes("dhcp")),n}function Ps(t){let n=new Map;for(let a of t.nodes)n.set(a.id,new Set([a.id]));for(let a of t.edges)n.get(a.source)?.add(a.target),n.get(a.target)?.add(a.source);return n}function Es(t){let n=Array.isArray(t.nodes)?t.nodes:[],a=Array.isArray(t.edges)?t.edges:[],l={nodes:[],edges:[],width:Ts,height:420,maxRank:0,maxLayerCount:0,backwardCount:0,interfaceCount:0};if(n.length===0)return l;let s=new Map;n.forEach((k,O)=>s.set(k.id,O));let o=n.length,f=Array.from({length:o},()=>[]),r=Array.from({length:o},()=>[]),b=[],w=new Set;for(let k of a){let O=s.get(k.source),le=s.get(k.target);if(O===void 0||le===void 0)continue;let se=`${O}\0${le}`;w.has(se)||(w.add(se),b.push({sourceIndex:O,targetIndex:le,source:k.source,target:k.target}),O!==le&&(f[O].push(le),r[le].push(O)))}let d=new Set;for(let k=0;k0&&f[k].every(O=>d.has(O))&&C.add(k);let h=[];for(let k=0;kA.has(O))),m.set(k,r[k].filter(O=>A.has(O)));let y=new Map,P=new Set,L=(k,O)=>{let le=[k];P.add(k),y.set(k,O);for(let se=0;se(m.get(k)?.length??0)===0);S.length===0&&h.length>0&&S.push(h[0]);for(let k of S)P.has(k)||L(k,0);for(let k of h)P.has(k)||L(k,0);let N=0;for(let k of y.values())N=Math.max(N,k);let p=N+1,T=N+(C.size>0?2:1),B=new Array(o).fill(0);for(let k of h)B[k]=y.get(k)??0;for(let k of C)B[k]=p;for(let k of d)B[k]=T;let K=0;for(let k of B)K=Math.max(K,k);let Z=Array.from({length:K+1},()=>[]);for(let k=0;kn[k].label||n[k].id;Z.forEach(k=>{k.sort((O,le)=>ie(O).localeCompare(ie(le)))});let x=new Array(o).fill(0),R=()=>{Z.forEach(k=>{k.forEach((O,le)=>{x[O]=le})})};R();let F=k=>{let O=Z[B[k]];return O.length<=1?.5:x[k]/(O.length-1)};for(let k=0;k<5;k+=1){for(let O=1;O<=K;O+=1){let le=Z[O],se=new Map;for(let fe of le){let ee=r[fe].filter(de=>B[de]0?ee.reduce((de,me)=>de+F(me),0)/ee.length:F(fe))}le.sort((fe,ee)=>(se.get(fe)??0)-(se.get(ee)??0)||ie(fe).localeCompare(ie(ee))),R()}for(let O=K-1;O>=0;O-=1){let le=Z[O],se=new Map;for(let fe of le){let ee=f[fe].filter(de=>B[de]>O);se.set(fe,ee.length>0?ee.reduce((de,me)=>de+F(me),0)/ee.length:F(fe))}le.sort((fe,ee)=>(se.get(fe)??0)-(se.get(ee)??0)||ie(fe).localeCompare(ie(ee))),R()}}let W=Math.max(...Z.map(k=>k.length),1),Q=Math.max(Ts,(W-1)*Cs+yt*2),g=Math.max(420,K*Ss+yt*2),oe=new Array(o).fill(Q/2),E=new Array(o).fill(yt);Z.forEach((k,O)=>{let le=Math.min(Q-yt*2,Math.max(0,(k.length-1)*Cs)),se=Q/2-le/2;k.forEach((fe,ee)=>{oe[fe]=k.length===1?Q/2:se+le*ee/(k.length-1),E[fe]=yt+O*Ss})});let z=n.map(k=>Number(k.width)||0),re=Math.min(...z),ue=Math.max(...z),v=n.map((k,O)=>{let le=Number(k.fill)||0,se=ue>re?(z[O]-re)/(ue-re):.5;return{id:k.id,label:k.label||k.id,x:oe[O],y:E[O],radius:7+se*11,color:le>=1&&le<=kn.length?kn[le-1]:"#64748b",rank:B[O],isInterface:Mi.test(k.id)}}),M=b.map(k=>({source:k.source,target:k.target,backward:B[k.sourceIndex]>=B[k.targetIndex]}));return{nodes:v,edges:M,width:Q,height:g,maxRank:K,maxLayerCount:W,backwardCount:M.filter(k=>k.backward).length,interfaceCount:v.filter(k=>k.isInterface).length}}function wn(t,n,a){return Math.min(a,Math.max(n,t))}function Ls({graph:t}){let n=We(()=>Es(t),[t]),[a,l]=c(""),s=We(()=>Fi(n),[n]),o=We(()=>Bi(n),[n]),f=We(()=>[...s,...o],[s,o]),r=We(()=>Ps(n),[n]),b=We(()=>{if(!a)return null;let v=f.find(k=>k.id===a);if(!v)return null;let M=new Set(v.nodeIDs);for(let k of v.nodeIDs)for(let O of r.get(k)??[])M.add(O);return M},[a,f,r]),w=We(()=>b?{...t,nodes:t.nodes.filter(v=>b.has(v.id)),edges:t.edges.filter(v=>b.has(v.source)&&b.has(v.target))}:t,[t,b]),d=We(()=>Es(w),[w]),[C,h]=c(()=>({x:0,y:0,w:d.width,h:d.height})),[A,I]=c(!1),[m,y]=c(null),[P,L]=c(!1),S=ne(null),N=ne(null);G(()=>{a&&!f.some(v=>v.id===a)&&l("")},[f,a]),G(()=>{N.current=null,I(!1),y(null),h({x:0,y:0,w:d.width,h:d.height})},[d]);let p=v=>{let{x:k,y:O,w:le,h:se}=v;return le>=d.width?k=(d.width-le)/2:k=wn(k,-40,d.width-le+40),se>=d.height?O=(d.height-se)/2:O=wn(O,-40,d.height-se+40),{x:k,y:O,w:le,h:se}},T=(v,M,k)=>{h(O=>{let le=Math.max(Vi,d.width*.06),se=d.width*1.08,ee=wn(O.w/v,le,se)/O.w,de=O.w*ee,me=O.h*ee;return p({x:M-(M-O.x)*ee,y:k-(k-O.y)*ee,w:de,h:me})})},B=(v,M)=>{let k=S.current;if(!k)return{x:0,y:0};let O=k.getScreenCTM();if(!O)return{x:0,y:0};let le=k.createSVGPoint();le.x=v,le.y=M;let se=le.matrixTransform(O.inverse());return{x:se.x,y:se.y}},K=()=>{h({x:0,y:0,w:d.width,h:d.height})},Z=v=>{v.preventDefault();let M=B(v.clientX,v.clientY);T(Math.exp(-v.deltaY*.0015),M.x,M.y)},ie=v=>{if(v.pointerType==="mouse"&&v.button!==0)return;let M=S.current;if(!M)return;let k=M.getScreenCTM();k&&(v.preventDefault(),M.setPointerCapture(v.pointerId),N.current={pointerId:v.pointerId,startX:v.clientX,startY:v.clientY,startView:C,scaleX:k.a||1,scaleY:k.d||1},I(!0))},x=v=>{let M=N.current;if(!M||M.pointerId!==v.pointerId)return;let k=(M.startX-v.clientX)/M.scaleX,O=(M.startY-v.clientY)/M.scaleY;h(p({...M.startView,x:M.startView.x+k,y:M.startView.y+O}))},R=v=>{let M=N.current;if(!M||M.pointerId!==v.pointerId)return;N.current=null,I(!1);let k=S.current;k?.hasPointerCapture(v.pointerId)&&k.releasePointerCapture(v.pointerId)},F=We(()=>new Map(d.nodes.map(v=>[v.id,v])),[d]),W=We(()=>Ps(d),[d]);if(d.nodes.length===0)return e("div",{class:"empty vpp-graph-empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"No active VPP nodes"}),e("p",{children:"Generate traffic or refresh after VPP has processed packets since the last stats clear."})]});let g=d.nodes.length<=40||P||m!==null,oe=m?W.get(m)??new Set([m]):null,E=m&&oe?Math.max(0,oe.size-1):0,z=Math.max(9,Math.min(26,C.w/70)),re=Math.max(1,Math.round(d.width/C.w*100)),ue=m?F.get(m):null;return e("div",{class:"vpp-graph-wrap",children:[e("div",{class:"vpp-graph-meta",children:[e("span",{children:[d.nodes.length," active node",d.nodes.length===1?"":"s"," \xB7 ",d.edges.length," ","edge",d.edges.length===1?"":"s",d.backwardCount>0?` \xB7 ${d.backwardCount} feedback`:"",d.interfaceCount>0?` \xB7 ${d.interfaceCount} interface${d.interfaceCount===1?"":"s"}`:""]}),e("span",{class:"vpp-graph-legend",children:[e("i",{class:"vpp-legend-node"}),"node = cycles/packet",e("i",{class:"vpp-legend-packet"}),"color = packets/call",e("i",{class:"vpp-legend-interface"}),"ring = interface",ue?` \xB7 ${ue.label}${ue.isInterface?" (interface)":""} \xB7 ${E} connected`:" \xB7 hover to isolate connections"]})]}),e("div",{class:"vpp-graph-toolbar",children:[e("span",{class:"vpp-graph-hint",children:["Inputs at top \xB7 outputs at bottom \xB7 drag to pan \xB7 scroll to zoom",d.backwardCount>0?" \xB7 dashed edge = feedback/self-loop":""]}),e("div",{class:"vpp-graph-controls",children:[f.length>0?e("label",{class:"vpp-interface-picker",children:[e("span",{children:"Focus"}),e("select",{value:a,title:"Show only nodes related to this interface or packet feature",onChange:v=>l(v.currentTarget.value),children:[e("option",{value:"",children:"All nodes"}),s.length>0?e("optgroup",{label:"Interfaces",children:s.map(v=>e("option",{value:v.id,children:v.label},v.id))}):null,o.length>0?e("optgroup",{label:"Packet features",children:o.map(v=>e("option",{value:v.id,children:v.label},v.id))}):null]})]}):null,e("div",{class:"vpp-graph-zoom",children:[e("button",{type:"button","aria-label":"Zoom out",title:"Zoom out",onClick:()=>T(1/1.35,C.x+C.w/2,C.y+C.h/2),children:"\u2212"}),e("span",{children:[re,"%"]}),e("button",{type:"button","aria-label":"Zoom in",title:"Zoom in",onClick:()=>T(1.35,C.x+C.w/2,C.y+C.h/2),children:"+"}),e("button",{type:"button",onClick:K,children:"Fit"}),d.nodes.length>40?e("button",{type:"button","aria-pressed":P,class:P?"is-active":"",onClick:()=>L(v=>!v),children:"Labels"}):null]})]})]}),e("svg",{ref:S,class:`vpp-graph${A?" is-panning":""}`,viewBox:`${C.x} ${C.y} ${C.w} ${C.h}`,role:"img","aria-label":"Active VPP packet processing graph, inputs at top and outputs at bottom",onWheel:Z,onPointerDown:ie,onPointerMove:x,onPointerUp:R,onPointerCancel:R,onLostPointerCapture:()=>{N.current=null,I(!1)},children:[e("defs",{children:e("marker",{id:"vpp-graph-arrow",viewBox:"0 0 10 10",refX:"9",refY:"5",markerWidth:"6",markerHeight:"6",orient:"auto-start-reverse",children:e("path",{d:"M 0 0 L 10 5 L 0 10 z",fill:"#64748b"})})}),e("g",{class:"vpp-graph-edges",children:d.edges.map((v,M)=>{let k=F.get(v.source),O=F.get(v.target);if(!k||!O)return null;let le=m!==null&&(v.source===m||v.target===m),se=m!==null&&!le;if(v.source===v.target){let pe=k.radius+6;return e("path",{d:`M ${k.x-k.radius} ${k.y} C ${k.x-pe*2.2} ${k.y-pe*2.4}, ${k.x+pe*2.2} ${k.y-pe*2.4}, ${k.x+k.radius} ${k.y}`,class:`vpp-edge vpp-edge-back${le?" vpp-edge-active":""}${se?" vpp-edge-hidden":""}`,markerEnd:"url(#vpp-graph-arrow)",vectorEffect:"non-scaling-stroke"},`${v.source}->${v.target}-${M}`)}let fe=O.x-k.x,ee=O.y-k.y,de=Math.max(1,Math.sqrt(fe*fe+ee*ee)),me=k.x+fe/de*(k.radius+2),u=k.y+ee/de*(k.radius+2),_=O.x-fe/de*(O.radius+5),$=O.y-ee/de*(O.radius+5),j=$>=u?1:-1,J=Math.max(24,Math.min(80,Math.abs($-u)*.45));return e("path",{d:`M ${me} ${u} C ${me} ${u+j*J}, ${_} ${$-j*J}, ${_} ${$}`,class:`vpp-edge${v.backward?" vpp-edge-back":""}${le?" vpp-edge-active":""}${se?" vpp-edge-hidden":""}`,markerEnd:"url(#vpp-graph-arrow)",vectorEffect:"non-scaling-stroke"},`${v.source}->${v.target}-${M}`)})}),e("g",{class:"vpp-graph-nodes",children:d.nodes.map(v=>{let M=v.x<160?"start":v.x>d.width-160?"end":"middle",k=M==="start"?v.x-v.radius:M==="end"?v.x+v.radius:v.x,O=oe===null||oe.has(v.id);return e("g",{class:`${g?"vpp-node":"vpp-node vpp-node-hover-label"}${O?"":" vpp-node-hidden"}`,onMouseEnter:()=>y(v.id),onMouseLeave:()=>y(null),children:[v.isInterface?e("circle",{class:"vpp-interface-ring",cx:v.x,cy:v.y,r:v.radius+4,vectorEffect:"non-scaling-stroke"}):null,e("circle",{cx:v.x,cy:v.y,r:v.radius,fill:v.color,vectorEffect:"non-scaling-stroke",children:e("title",{children:[v.label,v.isInterface?" (interface)":""]})}),e("text",{x:k,y:v.y+v.radius+z*1.25,textAnchor:M,style:{fontSize:`${z}px`},children:v.label})]},v.id)})})]}),e("div",{class:"vpp-graph-palette","aria-hidden":"true",children:[e("span",{children:"packets/call"}),kn.map((v,M)=>e("i",{style:{background:v},title:`${M+1}`},v)),e("span",{class:"vpp-graph-palette-note",children:"YlOrRd"})]})]})}function Ot(t,n){return`pipe${t.instance}.${n}`}function pt(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function Ut(t){let n=Math.trunc(Number(t)||0);return n>=1024**3?`${Math.trunc(n/1024**3)} GB`:n>=1024**2?`${Math.trunc(n/1024**2)} MB`:n>=1024?`${Math.trunc(n/1024)} KB`:`${n} B`}var Cn=!1,Is=0,As=[{id:"hosts",label:"Host interfaces"},{id:"hosttaps",label:"Host taps"},{id:"networks",label:"Networks"},{id:"pipes",label:"BD pipes"},{id:"interfaces",label:"Interfaces"},{id:"acl",label:"ACLs"},{id:"routes",label:"Route tables"},{id:"troubleshoot",label:"Troubleshoot"}];function Ns(){let[t,n]=c([]),[a,l]=c([]),[s,o]=c([]),[f,r]=c([]),[b,w]=c([]),[d,C]=c([]),[h,A]=c([]),[I,m]=c([]),[y,P]=c(!1),[L,S]=c(!0),[N,p]=c(""),[T,B]=c(""),[K,Z]=c(""),[ie,x]=c(!1),[R,F]=c(""),[W,Q]=c(""),[g,oe]=c(""),[E,z]=c(!1),[re,ue]=c(""),[v,M]=c(""),[k,O]=c(""),[le,se]=c(!1),[fe,ee]=c(null),[de,me]=c(!1),[u,_]=c(null),[$,j]=c(null),[J,pe]=c(null),[Le,q]=c(null),[he,ve]=c(null),[X,$e]=c([]),[H,_e]=c(""),[Ee,U]=c(!1),[we,Te]=c(null),[xe,Oe]=c(null),[et,Ke]=c(null),[ct,Ve]=c(null),[Ie,je]=c("hosts"),[Ae,ft]=c(null),[it,xn]=c(""),[Kt,Vn]=c(!1),jt=ne(!1),Mn=ne(0),[lo,co]=c([]),[Jt,$n]=c(""),[Rn,Fn]=c(!1),[uo,po]=c({}),[fo,Bn]=c({}),[kt,Ct]=c(""),Xt=ne(!1),Zt=ne(null),Hn=Ne("network"),On=Ne("network_pipe"),Un=Ne("host_interface"),qn=Ne("host_tap"),Wn=Ne("interface"),zn=Ne("acl"),Gn=Ne("route_table"),tt=ze();G(()=>{let[i,D,Y]=tt.segments;if(D!=="hosttaps"&&z(!1),D!=="pipes"&&x(!1),D!=="hosts"&&D!=="hosttaps"&&pe(null),i==="networks"){if(me(!1),_(null),j(null),q(null),ve(null),U(!1),Te(null),Ke(null),D==="new"){ee(null),se(!0),je("networks");return}if(D&&Y==="edit"){let ce=t.find(be=>String(be.id)===D);if(ce){ee(ce),se(!0),je("networks");return}}se(!1),ee(null),D&&As.some(ce=>ce.id===D)?je(D):je("networks");return}if(se(!1),ee(null),i==="interfaces"){if(je("interfaces"),U(!1),Te(null),D==="new"){me(!0),_(null),j(null),ve(null),Ke(null);return}let ce=b.find(be=>String(be.id)===D);me(!1),j(null),_(ce&&Y==="attach"?ce:null),ve(ce&&Y==="acls"?ce:null),Ke(ce&&Y==="route-tables"?ce:null);return}if(i==="acls"){if(je("acl"),me(!1),_(null),j(null),ve(null),Ke(null),U(!1),Te(null),D==="new"){q({acl:null});return}let ce=d.find(be=>String(be.id)===D);q(ce&&Y==="edit"?{acl:ce}:null);return}if(i==="route-tables"){if(je("routes"),me(!1),_(null),j(null),q(null),ve(null),Ke(null),D==="new"){U(!0),Te(null);return}if(U(!1),Y==="routes"||!Y){let ce=tt.query.get("ipv6")==="true",be=X.find(He=>String(He.id)===D&&He.ipv6===ce);Te(be??null),be?tt.segments[3]==="new"?Oe({table:be}):tt.segments[3]&&tt.segments[4]==="edit"?Oe({table:be,routeID:Number(tt.segments[3])}):Oe(null):Oe(null)}else Te(null),Oe(null);return}me(!1),Oe(null),_(null),j(null),q(null),ve(null),U(!1),Te(null),Ke(null)},[tt.path,tt.search,b,d,t,X]);let Be=i=>{i.networks&&(n(i.networks),window.dispatchEvent(new CustomEvent("rookery:networks-changed",{detail:i.networks}))),i.interfaces&&(w(i.interfaces),window.dispatchEvent(new CustomEvent("rookery:interfaces-changed",{detail:i.interfaces}))),i.acls&&C(i.acls),i.vms&&A(i.vms),i.vnfs&&m(i.vnfs)},Kn=async(i=!0)=>{try{let D=await V("/api/networks"),Y=Array.isArray(D)?D:[];Y.sort((ce,be)=>ce.name.localeCompare(be.name,void 0,{sensitivity:"base"})),Be({networks:Y}),i&&p("")}catch(D){i&&p(D instanceof Error?D.message:String(D))}},mo=async(i=!0)=>{try{let D=await V("/api/network-pipes"),Y=Array.isArray(D)?D:[];Y.sort((ce,be)=>ce.id-be.id),l(Y),i&&B("")}catch(D){i&&B(D instanceof Error?D.message:String(D))}},Yt=async(i=!0)=>{try{let D=await V("/api/host/interfaces"),Y=Array.isArray(D)?D:[];Y.sort((ce,be)=>ce.name.localeCompare(be.name,void 0,{sensitivity:"base"})),o(Y),i&&F("")}catch(D){i&&F(D instanceof Error?D.message:String(D))}},ho=async(i=!0)=>{try{let D=await V("/api/host/taps"),Y=Array.isArray(D)?D:[];Y.sort((ce,be)=>ce.name.localeCompare(be.name,void 0,{sensitivity:"base"})),r(Y),i&&Q("")}catch(D){i&&Q(D instanceof Error?D.message:String(D))}},bo=async i=>{if(window.confirm(`Delete host tap ${i.name}? The host will lose its connection to ${i.network_name}.`)){oe(i.name),Q("");try{await V(`/api/host/taps/${i.id}`,{method:"DELETE"}),r(D=>D.filter(Y=>Y.id!==i.id)),ae(`Deleted host tap ${i.name}`)}catch(D){let Y=D instanceof Error?D.message:String(D);Q(`Could not delete ${i.name}: ${Y}`),ae(`Could not delete host tap ${i.name}: ${Y}`,!0)}finally{oe("")}}},go=async i=>{if(window.confirm(`Bring ${i.name} into VPP? -This flushes host IP addresses on the NIC; the host will lose connectivity on it.`)){ce(i.name),F("");try{await V(`/api/host/interfaces/${encodeURIComponent(i.name)}/attach`,{method:"POST"}),oe(`Attached ${i.name} to VPP`),await jt()}catch(D){F(D instanceof Error?D.message:String(D))}finally{ce("")}}},go=async i=>{if(window.confirm(`Detach ${i.name} from VPP? +This flushes host IP addresses on the NIC; the host will lose connectivity on it.`)){ue(i.name),F("");try{await V(`/api/host/interfaces/${encodeURIComponent(i.name)}/attach`,{method:"POST"}),ae(`Attached ${i.name} to VPP`),await Yt()}catch(D){F(D instanceof Error?D.message:String(D))}finally{ue("")}}},vo=async i=>{if(window.confirm(`Detach ${i.name} from VPP? -The Linux NIC becomes available to the host again; you may need to renew its DHCP lease.`)){ce(i.name),F("");try{await V(`/api/host/interfaces/${encodeURIComponent(i.name)}`,{method:"DELETE"}),oe(`Detached ${i.name} from VPP`),await jt()}catch(D){F(D instanceof Error?D.message:String(D))}finally{ce("")}}},Kn=async(i=!0)=>{try{let D=await V("/api/interfaces"),j=Array.isArray(D)?D:[];j.sort((re,he)=>re.name.localeCompare(he.name,void 0,{sensitivity:"base"})),Be({interfaces:j}),i&&$("")}catch(D){i&&$(D instanceof Error?D.message:String(D))}},vo=async(i=!0)=>{try{let D=await V("/api/acls"),j=Array.isArray(D)?D:[];j.sort((re,he)=>re.name.localeCompare(he.name,void 0,{sensitivity:"base"})),Be({acls:j}),i&&U("")}catch(D){i&&U(D instanceof Error?D.message:String(D))}},ct=async(i=!0)=>{try{let D=await V("/api/route-tables"),j=(Array.isArray(D)?D:[]).map(re=>({...re,id:re.id??0,table_id:re.table_id??0,ipv6:!!re.ipv6,interface_count:re.interface_count??0,route_count:re.route_count??0}));$e(j),i&&_e("")}catch(D){i&&_e(D instanceof Error?D.message:String(D))}},Yt=async()=>{try{let i=await V("/api/vms");Be({vms:Array.isArray(i)?i:[]})}catch{Be({vms:[]})}},Jn=async()=>{try{let i=await V("/api/vnfs");Be({vnfs:Array.isArray(i)?i:[]})}catch{Be({vnfs:[]})}},yo=async()=>{try{let i=await V("/api/status");P(!!i.vpp?.ready)}catch{P(!1)}},rt=async()=>{S(!0),await Promise.allSettled([Gn(),fo(),jt(),mo(),Kn(),vo(),ct(),Yt(),Jn(),yo()]),S(!1)},Xn=async(i=!1)=>{if(!(Kt.current||Date.now()-Vn.current<(i?1e3:5e3))){Kt.current=!0,xn(!0);try{let j=await V("/api/troubleshoot/vpp-graph");ft(j),Dn("")}catch(j){Dn(j instanceof Error?j.message:String(j))}finally{Vn.current=Date.now(),Kt.current=!1,xn(!1)}}},Qt=async(i=!1)=>{let D=Date.now();if(!(kn||!i&&D-La<5e3)){kn=!0,Rn(!1);try{let j=await V("/api/troubleshoot/acl-counters");lo(Array.isArray(j)?j:[]),Mn("")}catch(j){Mn(j instanceof Error?j.message:String(j))}finally{kn=!1,La=Date.now(),Rn(!0)}}},Zn=async()=>{if(!Xt.current){Xt.current=!0;try{let i=await V("/api/interfaces/counters"),D=Array.isArray(i)?i:[],j=Date.now(),re=Zt.current,he={},He={},qe=re&&j>re.at?(j-re.at)/1e3:0;for(let Qe of D){let ut=Number(Qe.rx_bytes)||0,Xe=Number(Qe.tx_bytes)||0;he[Qe.name]={rx:ut,tx:Xe};let nt=re?.bytes[Qe.name];nt&&qe>0&&(He[Qe.name]={rx:ut>=nt.rx?(ut-nt.rx)/qe:0,tx:Xe>=nt.tx?(Xe-nt.tx)/qe:0})}Zt.current={at:j,bytes:he};let Ue={};for(let Qe of D)Ue[Qe.name]=Qe;uo(Ue),Fn(He),Ct("")}catch(i){Ct(i instanceof Error?i.message:String(i))}finally{Xt.current=!1}}};z(()=>{let i=tt.segments[0],D=i==="networks"||i==="interfaces"||i==="acls"||i==="route-tables",j=document.getElementById("networks-panel");if(Le!=="interfaces"||!D||!g||j!==null&&j.hidden)return;let re=()=>{document.hidden||Zn()};re();let he=window.setInterval(re,2e3);return()=>{window.clearInterval(he)}},[Le,tt.path,g]);let _o=async()=>{if(window.confirm(`Reset VPP interface counters? +The Linux NIC becomes available to the host again; you may need to renew its DHCP lease.`)){ue(i.name),F("");try{await V(`/api/host/interfaces/${encodeURIComponent(i.name)}`,{method:"DELETE"}),ae(`Detached ${i.name} from VPP`),await Yt()}catch(D){F(D instanceof Error?D.message:String(D))}finally{ue("")}}},jn=async(i=!0)=>{try{let D=await V("/api/interfaces"),Y=Array.isArray(D)?D:[];Y.sort((ce,be)=>ce.name.localeCompare(be.name,void 0,{sensitivity:"base"})),Be({interfaces:Y}),i&&M("")}catch(D){i&&M(D instanceof Error?D.message:String(D))}},yo=async(i=!0)=>{try{let D=await V("/api/acls"),Y=Array.isArray(D)?D:[];Y.sort((ce,be)=>ce.name.localeCompare(be.name,void 0,{sensitivity:"base"})),Be({acls:Y}),i&&O("")}catch(D){i&&O(D instanceof Error?D.message:String(D))}},dt=async(i=!0)=>{try{let D=await V("/api/route-tables"),Y=(Array.isArray(D)?D:[]).map(ce=>({...ce,id:ce.id??0,table_id:ce.table_id??0,ipv6:!!ce.ipv6,interface_count:ce.interface_count??0,route_count:ce.route_count??0}));$e(Y),i&&_e("")}catch(D){i&&_e(D instanceof Error?D.message:String(D))}},Qt=async()=>{try{let i=await V("/api/vms");Be({vms:Array.isArray(i)?i:[]})}catch{Be({vms:[]})}},Jn=async()=>{try{let i=await V("/api/vnfs");Be({vnfs:Array.isArray(i)?i:[]})}catch{Be({vnfs:[]})}},_o=async()=>{try{let i=await V("/api/status");P(!!i.vpp?.ready)}catch{P(!1)}},rt=async()=>{S(!0),await Promise.allSettled([Kn(),mo(),Yt(),ho(),jn(),yo(),dt(),Qt(),Jn(),_o()]),S(!1)},Xn=async(i=!1)=>{if(!(jt.current||Date.now()-Mn.current<(i?1e3:5e3))){jt.current=!0,Vn(!0);try{let Y=await V("/api/troubleshoot/vpp-graph");ft(Y),xn("")}catch(Y){xn(Y instanceof Error?Y.message:String(Y))}finally{Mn.current=Date.now(),jt.current=!1,Vn(!1)}}},en=async(i=!1)=>{let D=Date.now();if(!(Cn||!i&&D-Is<5e3)){Cn=!0,Fn(!1);try{let Y=await V("/api/troubleshoot/acl-counters");co(Array.isArray(Y)?Y:[]),$n("")}catch(Y){$n(Y instanceof Error?Y.message:String(Y))}finally{Cn=!1,Is=Date.now(),Fn(!0)}}},Zn=async()=>{if(!Xt.current){Xt.current=!0;try{let i=await V("/api/interfaces/counters"),D=Array.isArray(i)?i:[],Y=Date.now(),ce=Zt.current,be={},He={},qe=ce&&Y>ce.at?(Y-ce.at)/1e3:0;for(let Qe of D){let ut=Number(Qe.rx_bytes)||0,Je=Number(Qe.tx_bytes)||0;be[Qe.name]={rx:ut,tx:Je};let nt=ce?.bytes[Qe.name];nt&&qe>0&&(He[Qe.name]={rx:ut>=nt.rx?(ut-nt.rx)/qe:0,tx:Je>=nt.tx?(Je-nt.tx)/qe:0})}Zt.current={at:Y,bytes:be};let Ue={};for(let Qe of D)Ue[Qe.name]=Qe;po(Ue),Bn(He),Ct("")}catch(i){Ct(i instanceof Error?i.message:String(i))}finally{Xt.current=!1}}};G(()=>{let i=tt.segments[0],D=i==="networks"||i==="interfaces"||i==="acls"||i==="route-tables",Y=document.getElementById("networks-panel");if(Ie!=="interfaces"||!D||!y||Y!==null&&Y.hidden)return;let ce=()=>{document.hidden||Zn()};ce();let be=window.setInterval(ce,2e3);return()=>{window.clearInterval(be)}},[Ie,tt.path,y]);let wo=async()=>{if(window.confirm(`Reset VPP interface counters? -This clears the RX/TX counters for all VPP interfaces.`))try{await V("/api/interfaces/counters",{method:"DELETE"}),Zt.current=null,Fn({}),Ct(""),oe("Reset interface counters"),Zn()}catch(i){let D=i instanceof Error?i.message:String(i);Ct(D),oe(`Could not reset interface counters: ${D}`,!0)}};z(()=>{Le==="troubleshoot"&&Xn()},[Le]),z(()=>{rt();let i=qe=>{P(!!qe.detail)},D=()=>{Q("/networks/new")},j=qe=>{let Ue=qe.detail;Array.isArray(Ue)&&w(Ue)},re=qe=>{let Ue=qe.detail;Array.isArray(Ue)&&m(Ue)},he=qe=>{qe.detail==="networks"&&rt()},He=()=>{Gn(!1)};return window.addEventListener("rookery:vpp-ready",i),window.addEventListener("rookery:add-network",D),window.addEventListener("rookery:interfaces-data",j),window.addEventListener("rookery:vnfs-data",re),window.addEventListener("rookery:vnfs-changed",re),window.addEventListener("rookery:tab-changed",he),window.addEventListener("rookery:refresh-networks",He),()=>{window.removeEventListener("rookery:vpp-ready",i),window.removeEventListener("rookery:add-network",D),window.removeEventListener("rookery:interfaces-data",j),window.removeEventListener("rookery:vnfs-data",re),window.removeEventListener("rookery:vnfs-changed",re),window.removeEventListener("rookery:tab-changed",he),window.removeEventListener("rookery:refresh-networks",He)}},[]),z(()=>{let i=document.getElementById("network-tab-count");i&&(i.textContent=String(t.length))},[t.length]),z(()=>{let i=document.getElementById("new-network");i&&(i.disabled=!g,i.title=g?"":"Network creation is blocked until FD.io VPP is ready.")},[g]);let wo=async i=>{if(b.filter(he=>he.network_id===i.id).length>0){oe("Delete the virtual interfaces in this network first.",!0);return}let j=I.filter(he=>he.network_id===i.id);if(window.confirm(`Delete Rookery Network "${i.name}"? +This clears the RX/TX counters for all VPP interfaces.`))try{await V("/api/interfaces/counters",{method:"DELETE"}),Zt.current=null,Bn({}),Ct(""),ae("Reset interface counters"),Zn()}catch(i){let D=i instanceof Error?i.message:String(i);Ct(D),ae(`Could not reset interface counters: ${D}`,!0)}};G(()=>{Ie==="troubleshoot"&&Xn()},[Ie]),G(()=>{rt();let i=qe=>{P(!!qe.detail)},D=()=>{te("/networks/new")},Y=qe=>{let Ue=qe.detail;Array.isArray(Ue)&&w(Ue)},ce=qe=>{let Ue=qe.detail;Array.isArray(Ue)&&m(Ue)},be=qe=>{qe.detail==="networks"&&rt()},He=()=>{Kn(!1)};return window.addEventListener("rookery:vpp-ready",i),window.addEventListener("rookery:add-network",D),window.addEventListener("rookery:interfaces-data",Y),window.addEventListener("rookery:vnfs-data",ce),window.addEventListener("rookery:vnfs-changed",ce),window.addEventListener("rookery:tab-changed",be),window.addEventListener("rookery:refresh-networks",He),()=>{window.removeEventListener("rookery:vpp-ready",i),window.removeEventListener("rookery:add-network",D),window.removeEventListener("rookery:interfaces-data",Y),window.removeEventListener("rookery:vnfs-data",ce),window.removeEventListener("rookery:vnfs-changed",ce),window.removeEventListener("rookery:tab-changed",be),window.removeEventListener("rookery:refresh-networks",He)}},[]),G(()=>{let i=document.getElementById("network-tab-count");i&&(i.textContent=String(t.length))},[t.length]),G(()=>{let i=document.getElementById("new-network");i&&(i.disabled=!y,i.title=y?"":"Network creation is blocked until FD.io VPP is ready.")},[y]);let ko=async i=>{if(b.filter(be=>be.network_id===i.id).length>0){ae("Delete the virtual interfaces in this network first.",!0);return}let Y=I.filter(be=>be.network_id===i.id);if(window.confirm(`Delete Rookery Network "${i.name}"? -This removes the VPP bridge domain and ${j.length} VNF(s).`))try{await V(`/api/networks/${i.id}`,{method:"DELETE"});let he=t.filter(He=>He.id!==i.id);Be({networks:he}),p(""),oe(`Deleted network ${i.name}`),Jn()}catch(he){p(`Could not delete ${i.name}: ${he instanceof Error?he.message:String(he)}`)}},ko=async i=>{if(window.confirm(`Delete pipe between ${i.network_a_name} and ${i.network_b_name}?`)){J(String(i.id)),H("");try{await V(`/api/network-pipes/${i.id}`,{method:"DELETE"}),l(D=>D.filter(j=>j.id!==i.id)),oe(`Deleted pipe ${i.network_a_name} \u2194 ${i.network_b_name}`)}catch(D){H(D instanceof Error?D.message:String(D))}finally{J("")}}},Co=async i=>{if(i.attached_vm_id||i.attached_lightvm_id){oe("Detach the interface from its VM or LightVM first.",!0);return}if(window.confirm(`Delete virtual interface "${i.name}"? +This removes the VPP bridge domain and ${Y.length} VNF(s).`))try{await V(`/api/networks/${i.id}`,{method:"DELETE"});let be=t.filter(He=>He.id!==i.id);Be({networks:be}),p(""),ae(`Deleted network ${i.name}`),Jn()}catch(be){p(`Could not delete ${i.name}: ${be instanceof Error?be.message:String(be)}`)}},Co=async i=>{if(window.confirm(`Delete pipe between ${i.network_a_name} and ${i.network_b_name}?`)){Z(String(i.id)),B("");try{await V(`/api/network-pipes/${i.id}`,{method:"DELETE"}),l(D=>D.filter(Y=>Y.id!==i.id)),ae(`Deleted pipe ${i.network_a_name} \u2194 ${i.network_b_name}`)}catch(D){B(D instanceof Error?D.message:String(D))}finally{Z("")}}},So=async i=>{if(i.attached_vm_id||i.attached_lightvm_id){ae("Detach the interface from its VM or LightVM first.",!0);return}if(window.confirm(`Delete virtual interface "${i.name}"? -This removes the VPP vhost-user interface and socket.`))try{await V(`/api/interfaces/${i.id}`,{method:"DELETE"}),Be({interfaces:b.filter(j=>j.id!==i.id)}),$(""),oe(`Deleted interface ${i.name}`)}catch(j){$(`Could not delete ${i.name}: ${j instanceof Error?j.message:String(j)}`)}},So=async i=>{let D=i.attached_vm_id?h.find(j=>j.id===i.attached_vm_id):null;if(D&&(D.status==="running"||D.status==="starting")){oe("Stop the VM before detaching its interface.",!0);return}if(window.confirm(`Detach interface "${i.name}" from its VM?`))try{let j=await V(`/api/interfaces/${i.id}`,{method:"PUT",body:JSON.stringify({attached_vm_id:null})});Be({interfaces:b.map(re=>re.id===j.id?j:re)}),$(""),oe(`Detached ${i.name}`)}catch(j){$(`Could not detach ${i.name}: ${j instanceof Error?j.message:String(j)}`)}},To=async i=>{if(Bt(i,b).length>0){oe(`Remove ACL ${i.name} from its interfaces first.`,!0);return}if(window.confirm(`Delete ACL "${i.name}"?`))try{await V(`/api/acls/${i.id}`,{method:"DELETE"}),Be({acls:u.filter(D=>D.id!==i.id)}),U(""),oe(`Deleted ACL ${i.name}`)}catch(D){U(`Could not delete ${i.name}: ${D instanceof Error?D.message:String(D)}`),oe(`Could not delete ACL ${i.name}: ${D instanceof Error?D.message:String(D)}`,!0)}},Po=async i=>{if(!i.is_default&&window.confirm(`Delete route table "${i.name}" (${i.table_id}/${i.ipv6?"IPv6":"IPv4"})?`))try{await V(`/api/route-tables/${i.id}`,{method:"DELETE"}),await ct(),oe(`Deleted route table ${i.name}`)}catch(D){let j=D instanceof Error?D.message:String(D);_e(`Could not delete ${i.name}: ${j}`),oe(`Could not delete route table ${i.name}: ${j}`,!0)}},Eo=i=>{window.dispatchEvent(new CustomEvent("rookery:open-tab",{detail:"vnfs"})),window.dispatchEvent(new CustomEvent("rookery:add-vnf",{detail:{networkId:i.id}}))},Lo=b.filter(i=>i.attached_vm_id||i.attached_lightvm_id).length,Io=u.filter(i=>Bt(i,b).length>0).length,Ao=t.length===0?L?"Loading networks\u2026":"No Rookery Networks registered":`${t.length} network${t.length===1?"":"s"} \xB7 ${b.length} interface${b.length===1?"":"s"} \xB7 ${I.length} VNF${I.length===1?"":"s"}`,No=a.length===0?L?"Loading host interfaces\u2026":"No host interfaces found":`${a.length} host interface${a.length===1?"":"s"} \xB7 ${a.filter(i=>i.in_vpp).length} in VPP`,Do=f.length===0?L?"Loading host taps\u2026":"No network host taps":`${f.length} host tap${f.length===1?"":"s"} \xB7 ${new Set(f.map(i=>i.network_id)).size} network${new Set(f.map(i=>i.network_id)).size===1?"":"s"}`,xo=b.length===0?L?"Loading interfaces\u2026":"No virtual interfaces registered":`${b.length} interface${b.length===1?"":"s"} \xB7 ${Lo} attached`,Vo=u.length===0?L?"Loading ACLs\u2026":"No ACLs defined":`${u.length} ACL${u.length===1?"":"s"} \xB7 ${Io} assigned`,jn=i=>!i||!i.acl_indexes||i.acl_indexes.length===0?"no ACL":`${Ye(i.matched)} matched \xB7 ${Ye(i.denied)} denied \xB7 ${Ye(i.permitted)} permitted`,Mo=i=>{let D=i.acl_index??0,j=i.rule_index??0,he=u.find(He=>He.vpp_index===D)?.rules[j];return he?gn(he):`ACL ${D} rule ${j}`},Yn=(i,D)=>{let j=D?.rules||[];return e("div",{class:"acl-rule-counter-group",children:[e("span",{class:"acl-counter-direction",children:i}),!D?.acl_indexes||D.acl_indexes.length===0?e("span",{class:"acl-rule-counter muted",children:"No ACL applied"}):j.length===0?e("span",{class:"acl-rule-counter muted",children:"No explicit rule matches yet"}):j.map((re,he)=>e("span",{class:"acl-rule-counter",children:[e("span",{class:"acl-rule-counter-text",children:Mo(re)}),e("span",{class:"acl-rule-counter-value",children:[Ye(re.matched)," pkts \xB7 ",re.action||"\u2014"]})]},`${re.acl_index??0}-${re.rule_index??0}-${he}`))]})};return e(ge,{children:[e("div",{class:"subtabs",role:"tablist","aria-label":"Network sections",children:Ia.map(i=>e("button",{class:"subtab",type:"button",role:"tab","aria-selected":Le===i.id,onClick:()=>{Q(`/networks/${i.id}`),i.id==="interfaces"&&Qt()},children:i.label},i.id))}),e("section",{class:"panel","aria-labelledby":"networks-heading",hidden:Le!=="networks",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"networks-heading",children:"Network"}),e("p",{class:"panel-subtitle",children:Ao})]}),e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"})]}),N?e("div",{class:"banner",role:"alert",children:N}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Bridge domain"}),e("th",{scope:"col",children:"Interfaces"}),e("th",{scope:"col",children:"VNFs"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(i=>{let D=b.filter(re=>re.network_id===i.id).length,j=I.filter(re=>re.network_id===i.id);return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:i.name}),e("span",{class:"vm-description mono",children:[i.ipv4_cidr||"no IPv4 CIDR"," \xB7 ",i.ipv6_cidr||"no IPv6 CIDR"]}),i.bvi_host_name?e("span",{class:"vm-description",children:[i.bvi_host_name,i.ipv4_gateway?` \xB7 ${i.ipv4_gateway}`:""]}):null,e(De,{entityType:"network",entityID:i.id,name:i.name,labels:Bn.labels[String(i.id)],onSaved:re=>Bn.setEntityLabels(String(i.id),re)})]}),e("td",{class:"mono",children:["bd ",i.bridge_domain_id]}),e("td",{class:D?"mono":"mono muted",children:String(D)}),e("td",{class:j.length?"mono":"mono muted",children:j.length?j.map(re=>`${re.name} (${re.type})`).join(", "):"none"}),e("td",{class:"updated hide-sm",children:pt(i.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>Q(`/networks/${i.id}/edit`),children:"Edit CIDRs"}),e("button",{class:"row-action",type:"button",onClick:()=>Eo(i),children:"Add VNF"}),e("button",{class:"row-action danger",type:"button",disabled:D>0||!g,title:D>0?"Delete the virtual interfaces first.":g?"":"VPP must be ready to delete a bridge domain.",onClick:()=>{wo(i)},children:"Delete"})]})})]},i.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2B22"}),e("h3",{children:"No Networks yet"}),e("p",{children:"Create a VPP bridge domain to connect virtual interfaces and VMs."}),e("button",{class:"button primary",type:"button",disabled:!g,onClick:()=>Q("/networks/new"),children:"Create a network"})]})})]}),e("section",{class:"panel","aria-labelledby":"pipes-heading",hidden:Le!=="pipes",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"pipes-heading",children:"Bridge-domain pipes"}),e("p",{class:"panel-subtitle",children:"Connect two Rookery Networks with a VPP pipe pair."})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",disabled:!g||t.length<2,title:t.length<2?"Create at least two networks first":g?"":"VPP must be ready",onClick:()=>x(!0),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add pipe"]})]})]}),e("p",{class:"field-hint",children:"A pipe creates two VPP interfaces joined back-to-back. One end is placed in each bridge domain, providing an L2 bridge between the selected networks."}),t.length<2?e("p",{class:"field-hint",children:"Create at least two networks before adding a pipe."}):null,T?e("div",{class:"banner",role:"alert",children:T}):null,e("div",{class:"table-wrap",children:s.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Network A"}),e("th",{scope:"col",children:"Network B"}),e("th",{scope:"col",children:"VPP instance"}),e("th",{scope:"col",children:"Endpoints"}),e("th",{scope:"col",class:"hide-sm",children:"Created"}),e("th",{scope:"col",children:"Actions"})]})}),e("tbody",{children:s.map(i=>e("tr",{children:[e("td",{children:e("span",{class:"vm-name",children:i.network_a_name})}),e("td",{children:[e("span",{class:"vm-name",children:i.network_b_name}),e(De,{entityType:"network_pipe",entityID:i.id,name:`${i.network_a_name} \u2194 ${i.network_b_name}`,labels:Hn.labels[String(i.id)],onSaved:D=>Hn.setEntityLabels(String(i.id),D)})]}),e("td",{class:"mono",children:["pipe",i.instance]}),e("td",{class:"mono",children:[e("span",{children:[Ot(i,0)," (",i.endpoint_a_sw_if_index||"\u2014",")"]}),e("span",{children:" \u2194 "}),e("span",{children:[Ot(i,1)," (",i.endpoint_b_sw_if_index||"\u2014",")"]})]}),e("td",{class:"updated hide-sm",children:pt(i.created_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:!g||!i.endpoint_a_sw_if_index,onClick:()=>X({name:Ot(i,0),label:`pipe endpoint A \xB7 ${i.network_a_name}`}),children:"A addresses"}),e("button",{class:"button ghost",type:"button",disabled:!g||!i.endpoint_b_sw_if_index,onClick:()=>X({name:Ot(i,1),label:`pipe endpoint B \xB7 ${i.network_b_name}`}),children:"B addresses"}),e("button",{class:"row-action danger",type:"button",disabled:G===String(i.id)||!g,onClick:()=>{ko(i)},children:G===String(i.id)?"Deleting\u2026":"Delete"})]})})]},i.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2194"}),e("h3",{children:"No bridge-domain pipes"}),e("p",{children:"Connect two Rookery Networks to pass L2 traffic between their bridge domains."}),e("button",{class:"button primary",type:"button",disabled:!g||t.length<2,onClick:()=>x(!0),children:"Add pipe"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"hosts-heading",hidden:Le!=="hosts",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"hosts-heading",children:"Host interfaces"}),e("p",{class:"panel-subtitle",children:No})]}),e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"})]}),e("p",{class:"field-hint",children:["Physical NICs and external interfaces visible to the server; Rookery/VPP taps and host interfaces are hidden. \u201CIn VPP\u201D means a matching",e("code",{children:"host-"})," interface currently exists in VPP; the VPP state is shown as unknown when VPP cannot be queried. Attach takes a physical NIC into VPP (flushing its host addresses); persisted attachments are recreated after a VPP restart."]}),R?e("div",{class:"banner",role:"alert",children:R}):null,e("div",{class:"table-wrap",children:a.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Interface"}),e("th",{scope:"col",children:"Host / VPP addresses"}),e("th",{scope:"col",children:"Link"}),e("th",{scope:"col",children:"VPP"}),e("th",{scope:"col",children:"Default route"}),e("th",{scope:"col",children:"Type"}),e("th",{scope:"col",children:"Actions"})]})}),e("tbody",{children:a.map(i=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name mono",children:i.name}),e(De,{entityType:"host_interface",entityID:i.name,name:i.name,labels:On.labels[i.name],onSaved:D=>On.setEntityLabels(i.name,D)})]}),e("td",{class:"mono",children:e("div",{class:"address-stack",children:[e("span",{children:[e("span",{class:"address-side",children:"Host"}),i.addresses&&i.addresses.length>0?i.addresses.join(", "):"\u2014"]}),i.in_vpp?e("span",{children:[e("span",{class:"address-side",children:"VPP"}),i.vpp_addresses&&i.vpp_addresses.length>0?i.vpp_addresses.join(", "):"\u2014"]}):null]})}),e("td",{children:e("span",{class:"status-badge","data-status":i.up?"running":"disabled",children:i.up?"up":"down"})}),e("td",{children:i.vpp_checked?i.in_vpp?e(ge,{children:[e("span",{class:"status-badge","data-status":i.vpp_up?"running":"degraded",children:"in VPP"}),e("span",{class:"vm-description mono",children:[i.vpp_name||`host-${i.name}`,typeof i.vpp_sw_if_index=="number"?` \xB7 sw_if_index ${i.vpp_sw_if_index}`:"",i.managed?" \xB7 persisted":""]})]}):e("span",{class:"status-badge","data-status":"disabled",children:"not in VPP"}):e("span",{class:"status-badge","data-status":"disabled",children:"unknown"})}),e("td",{class:"mono",children:i.default_route?"yes":"\u2014"}),e("td",{children:i.wireless?"wireless":i.physical?"physical":"virtual"}),e("td",{children:e("div",{class:"actions",children:i.in_vpp?e(ge,{children:[i.managed?e(ge,{children:[e("button",{class:"button ghost",type:"button",disabled:!g,onClick:()=>X({name:i.vpp_name||`host-${i.name}`,label:"host interface",hostAddresses:i.addresses||[]}),children:"Addresses"}),e("button",{class:"button ghost",type:"button",disabled:!g,onClick:()=>fe({name:i.vpp_name||`host-${i.name}`,label:"Host interface"}),children:"Route tables"})]}):null,e("button",{class:"button ghost",type:"button",disabled:se===i.name||!g,onClick:()=>{go(i)},children:se===i.name?"Detaching\u2026":"Detach"})]}):e("button",{class:"button ghost",type:"button",disabled:se===i.name||!g||!i.physical||i.wireless||i.default_route,title:i.default_route?"The host default-route interface cannot be attached":i.wireless?"Wireless interfaces cannot be attached":i.physical?"Take this NIC into VPP":"Only physical interfaces can be attached",onClick:()=>{bo(i)},children:se===i.name?"Attaching\u2026":"Attach to VPP"})})})]},i.name))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u21C4"}),e("h3",{children:"No host interfaces found"}),e("p",{children:"Linux network interfaces will appear here when available."})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"hosttaps-heading",hidden:Le!=="hosttaps",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"hosttaps-heading",children:"Host taps"}),e("p",{class:"panel-subtitle",children:Do})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",disabled:!g||t.length===0,onClick:()=>M(!0),children:"Add host tap"})]})]}),e("p",{class:"field-hint",children:"Host taps bridge a Rookery Network into a Linux tap interface on the server. Assign a host IPv4 address here (or use DHCP on the network) and the host can reach VMs and other machines on that network directly. Host taps are persisted and recreated after an API or VPP restart."}),W?e("div",{class:"banner",role:"alert",children:W}):null,e("div",{class:"table-wrap",children:f.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Interface"}),e("th",{scope:"col",children:"Network"}),e("th",{scope:"col",children:"Host / VPP address"}),e("th",{scope:"col",children:"VPP index"}),e("th",{scope:"col",children:"Created"}),e("th",{scope:"col",children:"Actions"})]})}),e("tbody",{children:f.map(i=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name mono",children:i.name}),e(De,{entityType:"host_tap",entityID:i.id,name:i.name,labels:Un.labels[String(i.id)],onSaved:D=>Un.setEntityLabels(String(i.id),D)})]}),e("td",{children:[e("span",{class:"vm-name",children:i.network_name}),e("span",{class:"vm-description mono",children:["#",i.network_id]})]}),e("td",{class:"mono",children:e("div",{class:"address-stack",children:[e("span",{children:[e("span",{class:"address-side",children:"Host"}),i.ipv4_address||"\u2014"]}),e("span",{children:[e("span",{class:"address-side",children:"VPP"}),i.vpp_addresses&&i.vpp_addresses.length>0?i.vpp_addresses.join(", "):"\u2014"]})]})}),e("td",{class:"mono",children:i.sw_if_index||"\u2014"}),e("td",{children:pt(i.created_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:!g,onClick:()=>X({name:i.name,label:"host tap",hostAddresses:i.ipv4_address?[i.ipv4_address]:[]}),children:"Addresses"}),e("button",{class:"button ghost",type:"button",disabled:!g||!i.sw_if_index,onClick:()=>fe({name:i.name,label:"Host tap"}),children:"Route tables"}),e("button",{class:"button ghost",type:"button",disabled:y===i.name||!g,onClick:()=>{ho(i)},children:y===i.name?"Deleting\u2026":"Delete"})]})})]},i.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u21C4"}),e("h3",{children:"No host taps"}),e("p",{children:"Connect a Rookery Network to the Linux host to reach its machines."}),e("button",{class:"button primary",type:"button",disabled:!g||t.length===0,onClick:()=>M(!0),children:"Add host tap"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"interfaces-heading",hidden:Le!=="interfaces",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"interfaces-heading",children:"Rookery Virtual Interfaces"}),e("p",{class:"panel-subtitle",children:[xo,g?e("span",{class:"live-indicator","data-live":kt?"false":"true",children:kt?"counters unavailable":"live counters"}):null]})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",onClick:()=>{Qt(!0)},children:"Refresh counters"}),e("button",{class:"button ghost",type:"button",disabled:!g,title:g?"":"VPP must be ready to reset interface counters.",onClick:()=>{_o()},children:"Reset counters"}),e("button",{class:"button primary",type:"button",disabled:t.length===0||!g,title:t.length===0?"Create a Rookery Network first":g?"":"Interface creation is blocked until FD.io VPP is ready",onClick:()=>Q("/interfaces/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add interface"]})]})]}),v?e("div",{class:"banner",role:"alert",children:v}):null,kt?e("p",{class:"field-hint",children:["Live counters unavailable: ",kt]}):null,e("div",{class:"table-wrap",children:b.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Network"}),e("th",{scope:"col",children:"MAC"}),e("th",{scope:"col",children:"VPP addresses"}),e("th",{scope:"col",children:"Attached to"}),e("th",{scope:"col",children:"TX/RX"}),e("th",{scope:"col",children:"Speed"}),e("th",{scope:"col",children:"ACLs"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:b.map(i=>{let D=i.attached_vm_id?h.find(mt=>mt.id===i.attached_vm_id):null,j=!!i.attached_vm_id,re=!!(i.attached_vm_id||i.attached_lightvm_id),he=co[i.name],He=po[i.name],qe=!!(D&&(D.status==="running"||D.status==="starting")),Ue=[],Qe=i.ingress_acls?.length?i.ingress_acls:i.ingress_acl_name?[i.ingress_acl_name]:[],ut=i.egress_acls?.length?i.egress_acls:i.egress_acl_name?[i.egress_acl_name]:[];Qe.length>0&&Ue.push(`from: ${Qe.join(" \u2192 ")}`),ut.length>0&&Ue.push(`to: ${ut.join(" \u2192 ")}`);let Xe=ro.find(mt=>mt.interface===i.name),nt=dt===i.id;return e(ge,{children:[e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:i.name}),i.socket_path?e("span",{class:"vm-description",title:i.socket_path,children:i.socket_path}):null,e("span",{class:"vm-description",children:["FIB v4: ",i.ipv4_table_name||"default"," \xB7 v6:"," ",i.ipv6_table_name||"default"]}),e(De,{entityType:"interface",entityID:i.id,name:i.name,labels:qn.labels[String(i.id)],onSaved:mt=>qn.setEntityLabels(String(i.id),mt)})]}),e("td",{class:"mono",children:i.network_name||"\u2014"}),e("td",{class:"mono",children:i.mac||"\u2014"}),e("td",{class:"mono",children:i.vpp_addresses&&i.vpp_addresses.length>0?i.vpp_addresses.join(", "):"\u2014"}),e("td",{class:re?"mono":"mono muted",children:D?D.name:i.attached_vm_name||(i.attached_lightvm_name?`${i.attached_lightvm_name} (LightVM)`:"unattached")}),e("td",{class:he?"mono traffic-cell":"mono muted traffic-cell",children:he?e(ge,{children:[e("span",{class:"traffic-line",children:["\u2193 ",Ut(he.rx_bytes??0)]}),e("span",{class:"traffic-line",children:["\u2191 ",Ut(he.tx_bytes??0)]})]}):"\u2014"}),e("td",{class:He?"mono traffic-cell":"mono muted traffic-cell",children:He?e(ge,{children:[e("span",{class:"traffic-line",children:["\u2193 ",Ut(He.rx),"/s"]}),e("span",{class:"traffic-line",children:["\u2191 ",Ut(He.tx),"/s"]})]}):"\u2014"}),e("td",{class:Ue.length?"mono":"mono muted",children:Ue.length?e("div",{class:"interface-acl-summary",children:[e("span",{class:"acl-assignment",children:Ue.join(" \xB7 ")}),Xe?e("span",{class:"acl-inline-counter",children:["from ",Ye(Xe.ingress?.matched)," \xB7 to"," ",Ye(Xe.egress?.matched)]}):$n?Jt?e("span",{class:"acl-inline-counter muted",children:"counters unavailable"}):null:e("span",{class:"acl-inline-counter muted",children:"loading counters\u2026"}),e("button",{class:"row-action",type:"button",onClick:()=>Ve(nt?null:i.id),children:nt?"Hide details":"ACL details"})]}):"none"}),e("td",{class:"updated hide-sm",children:pt(i.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",disabled:!g,title:g?"":"VPP must be ready to set interface addresses.",onClick:()=>X({name:i.name,label:"vhost-user"}),children:"Addresses"}),e("button",{class:"row-action",type:"button",disabled:!!i.attached_lightvm_id,title:i.attached_lightvm_id?"Remove the interface from its LightVM first.":"",onClick:()=>Q(`/interfaces/${i.id}/attach`),children:"Attach"}),j?e("button",{class:"row-action",type:"button",disabled:qe,title:qe?"Stop the VM before detaching its interface.":"",onClick:()=>{So(i)},children:"Detach"}):null,!j&&i.attached_lightvm_id?e("button",{class:"row-action",type:"button",onClick:()=>Q(`/lightvm/instances/${i.attached_lightvm_id}/edit`),children:"Edit LightVM"}):null,e("button",{class:"row-action",type:"button",disabled:!g,title:g?"":"VPP must be ready to apply interface ACLs.",onClick:()=>Q(`/interfaces/${i.id}/acls`),children:"ACLs"}),e("button",{class:"row-action",type:"button",disabled:!g,title:g?"":"VPP must be ready to change interface FIB tables.",onClick:()=>Q(`/interfaces/${i.id}/route-tables`),children:"Route tables"}),e("button",{class:"row-action danger",type:"button",disabled:re||!g,title:re?"Detach the interface from its VM or LightVM first.":g?"":"VPP must be ready to delete a vhost-user interface.",onClick:()=>{Co(i)},children:"Delete"})]})})]}),nt&&Ue.length>0?e("tr",{class:"acl-details-row",children:e("td",{colSpan:10,children:e("div",{class:"interface-acl-details",children:[e("div",{class:"interface-acl-details-head",children:[e("strong",{children:["ACL details for ",i.name]}),e("button",{class:"button ghost",type:"button",onClick:()=>Q(`/interfaces/${i.id}/acls`),children:"Edit ACLs"})]}),Xe?e(ge,{children:[e("div",{class:"acl-details-summary",children:[e("span",{children:[e("b",{children:"From interface"})," ",jn(Xe.ingress)]}),e("span",{children:[e("b",{children:"To interface"})," ",jn(Xe.egress)]})]}),e("div",{class:"acl-rule-counter-list",children:[Yn("From interface rules",Xe.ingress),Yn("To interface rules",Xe.egress)]})]}):Jt?e("p",{class:"field-hint",children:["ACL counters unavailable: ",Jt]}):$n?e("p",{class:"field-hint",children:"No ACL counter entry for this interface yet. Generate traffic or refresh the interface list."}):e("p",{class:"field-hint",children:"Loading ACL counters\u2026"}),e("p",{class:"field-hint",children:"Counters show explicit ACL rule matches; implicit-deny packets are not included."})]})})}):null]},i.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u21C4"}),e("h3",{children:"No virtual interfaces yet"}),e("p",{children:"Add a VPP vhost-user interface to a Rookery Network, then attach it to a VM."})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"acls-heading",hidden:Le!=="acl",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"acls-heading",children:"Network ACLs"}),e("p",{class:"panel-subtitle",children:Vo})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",disabled:!g,title:g?"":"ACL changes are blocked until FD.io VPP is ready.",onClick:()=>Q("/acls/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add ACL"]})]})]}),C?e("div",{class:"banner",role:"alert",children:C}):null,e("div",{class:"table-wrap",children:u.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Title"}),e("th",{scope:"col",children:"Rules"}),e("th",{scope:"col",children:"Assigned to"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:u.map(i=>{let D=Bt(i,b);return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:i.name}),i.description?e("span",{class:"vm-description",title:i.description,children:i.description}):null,e(De,{entityType:"acl",entityID:i.id,name:i.name,labels:Wn.labels[String(i.id)],onSaved:j=>Wn.setEntityLabels(String(i.id),j)})]}),e("td",{children:i.title||"\u2014"}),e("td",{class:"mono",children:[i.rules?.length||0," rule",i.rules?.length===1?"":"s"]}),e("td",{class:D.length?"mono":"mono muted",children:D.length?D.map(j=>j.name).join(", "):"none"}),e("td",{class:"updated hide-sm",children:pt(i.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",disabled:!g,title:g?"":"VPP must be ready to edit ACLs.",onClick:()=>Q(`/acls/${i.id}/edit`),children:"Edit / Preview"}),e("button",{class:"row-action danger",type:"button",disabled:D.length>0,title:D.length>0?"Remove this ACL from its interfaces first.":"",onClick:()=>{To(i)},children:"Delete"})]})})]},i.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A4"}),e("h3",{children:"No ACLs yet"}),e("p",{children:"Define ordered ingress/egress rules, then assign an ACL to a virtual interface."}),e("button",{class:"button primary",type:"button",disabled:!g,onClick:()=>Q("/acls/new"),children:"Add an ACL"})]})})]}),e("section",{class:"panel panel-spaced","aria-label":"Route tables",hidden:Le!=="routes",children:xe?e(ga,{table:xe.table,routeID:xe.routeID,interfaces:b,bviInterfaces:t.filter(i=>!!i.bvi_host_name).map(i=>i.bvi_host_name||"").filter(Boolean),hostInterfaces:a,hostTaps:f,tables:K,onBack:()=>{Oe(null),Q(`/route-tables/${xe.table.id}?ipv6=${xe.table.ipv6}`,!0)},onSaved:()=>{let i=xe.table;Oe(null),Q(`/route-tables/${i.id}?ipv6=${i.ipv6}`,!0),oe(xe.routeID?`Saved route ${i.name}`:`Added route to ${i.name}`),ct(!1)}},`route-form-${xe.table.id}-${xe.routeID??"new"}`):we?e(ya,{table:we,onBack:()=>{Te(null),Q("/networks/routes",!0)},onChanged:()=>{ct(!1)}},`route-page-${we.ipv6?"v6":"v4"}-${we.id}`):Ee?e(va,{tables:K,onCancel:()=>{q(!1),Q("/networks/routes",!0)},onSaved:i=>{q(!1),Q("/networks/routes",!0),oe(`Created route table ${i.name}`),ct()}}):e(ge,{children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"routes-heading",children:"Route tables"}),e("p",{class:"panel-subtitle",children:"IP FIB / VRF definitions attachable to interfaces. Table 0 is the default VRF."})]}),e("button",{class:"button primary",type:"button",disabled:!g,onClick:()=>Q("/route-tables/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add route table"]})]}),O?e("div",{class:"banner",role:"alert",children:O}):null,e("div",{class:"table-wrap",children:e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Family"}),e("th",{scope:"col",children:"Table ID"}),e("th",{scope:"col",children:"Interfaces"}),e("th",{scope:"col",children:"Routes"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:K.map(i=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:i.name}),i.is_default?e("span",{class:"vm-description",children:"default VRF"}):null,i.network_id?e("span",{class:"vm-description",children:["network VRF \xB7 network ",i.network_id]}):null,i.is_default?null:e(De,{entityType:"route_table",entityID:i.id,name:i.name,labels:zn.labels[String(i.id)],onSaved:D=>zn.setEntityLabels(String(i.id),D)})]}),e("td",{class:"mono",children:i.ipv6?"IPv6":"IPv4"}),e("td",{class:"mono",children:i.table_id}),e("td",{class:i.interface_count?"mono":"mono muted",children:i.interface_count||0}),e("td",{class:i.route_count?"mono":"mono muted",children:i.route_count||0}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>Q(`/route-tables/${i.id}?ipv6=${i.ipv6}`),children:"Routes"}),i.is_default?e("span",{class:"muted",children:"built-in"}):i.managed?e("span",{class:"muted",children:"network-managed"}):e("button",{class:"row-action danger",type:"button",disabled:!g,onClick:()=>{Po(i)},children:"Delete"})]})})]},`${i.ipv6?"v6":"v4"}-${i.id}`))})]})})]})}),e("section",{class:"panel panel-spaced","aria-labelledby":"troubleshoot-heading",hidden:Le!=="troubleshoot",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"troubleshoot-heading",children:"Troubleshoot"}),e("p",{class:"panel-subtitle",children:"VPP packet processing graph"})]}),e("button",{class:"button ghost",type:"button",disabled:Gt,onClick:()=>{Xn(!0)},children:Gt?"Refreshing\u2026":"Refresh graph"})]}),e("p",{class:"field-hint",children:"Active VPP nodes since the last stats clear. Node size represents cycles per packet; color represents packets per call."}),it?e("div",{class:"banner",role:"alert",children:it}):null,Gt&&!Ae?e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"Loading VPP graph\u2026"})]}):Ae?e(Ea,{graph:Ae}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"No graph loaded"}),e("p",{children:"Use Refresh graph to inspect the active VPP packet-processing nodes."})]}),Ae?.generated_at?e("p",{class:"field-hint",children:["Sampled ",pt(Ae.generated_at)]}):null]}),e("section",{class:"panel panel-spaced","aria-labelledby":"trace-heading",hidden:Le!=="troubleshoot",children:[e("div",{class:"panel-head",children:e("div",{children:[e("h2",{id:"trace-heading",children:"Packet trace"}),e("p",{class:"panel-subtitle",children:"Trace packets through a selected VPP graph node"})]})}),e(ba,{active:Le==="troubleshoot"})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"capture-heading",hidden:Le!=="troubleshoot",children:[e("div",{class:"panel-head",children:e("div",{children:[e("h2",{id:"capture-heading",children:"Packet capture"}),e("p",{class:"panel-subtitle",children:"Capture VPP traffic and download pcap files"})]})}),e(ma,{})]}),ne?e(pa,{networks:t,onClose:()=>x(!1),onSaved:i=>{x(!1),l(D=>[...D,i].sort((j,re)=>j.id-re.id)),H(""),oe(`Created pipe ${i.network_a_name} \u2194 ${i.network_b_name}`)}}):null,de?e(ua,{network:pe,onClose:()=>{ie(!1),ee(null),Q("/networks",!0)},onSaved:i=>{let D=!!pe;ie(!1),ee(null),Q("/networks",!0);let j=D?t.map(re=>re.id===i.id?i:re):[...t,i];Be({networks:j.sort((re,he)=>re.name.localeCompare(he.name,void 0,{sensitivity:"base"}))}),p(""),oe(D?`Updated network ${i.name}`:`Created network ${i.name}`)}}):null,ue?e(da,{networks:t,vms:h,onClose:()=>{me(!1),Q("/networks/interfaces",!0)},onSaved:i=>{me(!1),Q("/networks/interfaces",!0),Be({interfaces:[...b,i].sort((D,j)=>D.name.localeCompare(j.name,void 0,{sensitivity:"base"}))}),$(""),oe(`Created interface ${i.name}`),Yt()}}):null,B?e(_a,{name:B.name,label:B.label,hostAddresses:B.hostAddresses,onClose:()=>X(null)},`addresses-${B.name}`):null,Z?e(wa,{name:Z.name,label:Z.label,tables:K,onClose:()=>fe(null),onSaved:i=>{fe(null),oe(`Saved route tables for ${i.name}`)}},`route-tables-${Z.name}`):null,c?e(oa,{iface:c,vms:h,onClose:()=>{_(null),Q("/networks/interfaces",!0)},onSaved:i=>{_(null),Q("/networks/interfaces",!0),Be({interfaces:b.map(D=>D.id===i.id?i:D)}),$(""),oe(i.attached_vm_id?`Attached ${i.name} to ${i.attached_vm_name||"VM"}`:`Detached ${i.name}`),Yt()}},`attach-${c.id}`):null,Ie?e(aa,{acl:Ie.acl,vppReady:g,onClose:()=>{le(null),Q("/networks/acl",!0)},onSaved:i=>{le(null),Q("/networks/acl",!0),Be({acls:Ie.acl?u.map(D=>D.id===i.id?i:D):[...u,i].sort((D,j)=>D.name.localeCompare(j.name,void 0,{sensitivity:"base"}))}),U(""),oe(`${Ie.acl?"Saved":"Added"} ACL ${i.name}`),Kn(!1)}},Ie.acl?`edit-${Ie.acl.id}`:"new"):null,be?e(la,{iface:be,acls:u,vppReady:g,onClose:()=>{ve(null),Q("/networks/interfaces",!0)},onSaved:i=>{ve(null),Q("/networks/interfaces",!0),Be({interfaces:b.map(D=>D.id===i.id?i:D)}),$(""),Qt(!0),oe(`Applied ACLs to ${i.name}`)}},`acl-${be.id}`):null,et?e(ca,{iface:et,tables:K,onClose:()=>{Ke(null),Q("/networks/interfaces",!0)},onSaved:i=>{Ke(null),Q("/networks/interfaces",!0),Be({interfaces:b.map(D=>D.id===i.id?i:D)}),$(""),oe(`Updated route tables for ${i.name}`),ct(!1)}},`route-tables-${et.id}`):null,E?e(ia,{networks:t,onClose:()=>M(!1),onSaved:i=>{M(!1),r(D=>[...D,i].sort((j,re)=>j.name.localeCompare(re.name,void 0,{sensitivity:"base"}))),Y(""),oe(`Created host tap ${i.name}`)}}):null]})}function Na(t){t&&Fe(e(Aa,{}),t)}function Va(t){if(!t)return[];let n=new Set;for(let s of t.split(",")){let l=s.trim();if(!l)continue;let[a,o,f]=l.split("-"),r=Number(a);if(!Number.isInteger(r)||r<0)continue;let b=f?Number(o):r;if(!(!Number.isInteger(b)||bs-l)}function Bi(t){let n=[...new Set(t)].filter(l=>Number.isInteger(l)&&l>=0).sort((l,a)=>l-a),s=[];for(let l=0;l0?e("div",{class:"cpu-grid",role:"group","aria-label":"Allowed CPU cores",children:n.map(f=>e("button",{type:"button",class:o.has(f)?"cpu-chip selected":"cpu-chip","aria-pressed":o.has(f),disabled:a,onClick:()=>s(Hi(t,f)),children:f},f))}):null,e("input",{type:"text",value:t,disabled:a,placeholder:"e.g. 0-3,7 (empty means all CPUs)",onInput:f=>s(f.currentTarget.value)}),l?e("span",{class:"field-hint",children:l}):null]})}var Ma=1024*1024;function Da(t){return t?String(Math.round(t/Ma*10)/10):""}function xa(t){let n=Number(t);return!Number.isFinite(n)||n<=0?0:Math.round(n*Ma)}function Cn(t){return t===void 0||t===0?"":String(t)}function Sn(t){let n=Number(t);return Number.isFinite(n)&&n>0?Math.floor(n):0}function Pn({limits:t,devices:n,onChange:s,disabled:l}){let a=new Map(n.map(u=>[u.path,u])),o=new Set(t.map(u=>u.device)),f=n.filter(u=>!o.has(u.path)),r=(u,k)=>{s(t.map((h,A)=>A===u?{...h,...k}:h))},b=()=>{let u=f[0];u&&s([...t,{device:u.path}])},w=u=>{s(t.filter((k,h)=>h!==u))};return e("div",{class:"cgroup-io-editor",children:[e("div",{class:"table-wrap",children:e("table",{class:"cgroup-io-table",children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Block device"}),e("th",{scope:"col",children:"Read MB/s"}),e("th",{scope:"col",children:"Write MB/s"}),e("th",{scope:"col",children:"Read IOPS"}),e("th",{scope:"col",children:"Write IOPS"}),e("th",{scope:"col",children:"Weight"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.length===0?e("tr",{children:e("td",{colspan:7,class:"muted",children:"No per-device I/O limits. Add a block device to constrain it."})}):t.map((u,k)=>e("tr",{children:[e("td",{children:e("select",{value:u.device,disabled:l,onChange:h=>r(k,{device:h.currentTarget.value}),children:[a.has(u.device)?null:e("option",{value:u.device,children:u.device}),n.map(h=>e("option",{value:h.path,disabled:o.has(h.path)&&h.path!==u.device,children:[h.path,h.model?` \xB7 ${h.model}`:""]},h.path))]})}),e("td",{children:e("input",{type:"number",min:0,step:"any",disabled:l,value:Da(u.read_bps),placeholder:"unlimited",onInput:h=>r(k,{read_bps:xa(h.currentTarget.value)})})}),e("td",{children:e("input",{type:"number",min:0,step:"any",disabled:l,value:Da(u.write_bps),placeholder:"unlimited",onInput:h=>r(k,{write_bps:xa(h.currentTarget.value)})})}),e("td",{children:e("input",{type:"number",min:0,step:1,disabled:l,value:Cn(u.read_iops),placeholder:"unlimited",onInput:h=>r(k,{read_iops:Sn(h.currentTarget.value)})})}),e("td",{children:e("input",{type:"number",min:0,step:1,disabled:l,value:Cn(u.write_iops),placeholder:"unlimited",onInput:h=>r(k,{write_iops:Sn(h.currentTarget.value)})})}),e("td",{children:e("input",{type:"number",min:0,max:1e4,step:1,disabled:l,value:Cn(u.weight),placeholder:"inherit",onInput:h=>r(k,{weight:Sn(h.currentTarget.value)})})}),e("td",{children:e("button",{class:"row-action danger",type:"button",disabled:l,onClick:()=>w(k),children:"Remove"})})]},u.device))})]})}),e("div",{class:"actions",children:e("button",{class:"button ghost",type:"button",disabled:l||f.length===0,onClick:b,children:"Add block device"})})]})}var $a=1024*1024;function En(t){return t?String(Math.round(t/$a)):""}function Ra(t){let n=Number(t);return!Number.isFinite(n)||n<=0?0:Math.round(n*$a)}function Fa(t){return t?String(t):""}function Ba(t){let n=Number(t);return Number.isFinite(n)&&n>0?Math.floor(n):0}function _t(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(1)} MB`:n>=1024?`${(n/1024).toFixed(1)} KB`:`${n} B`}function qt(t){return t===void 0?"\u2014":`${_t(t)}/s`}function Oi(t){let n=_t(t.memory_current_bytes);return t.memory_max_bytes>0?`${n} / ${_t(t.memory_max_bytes)}`:`${n} / unlimited`}function Ui(t,n){return n<=0?0:Math.max(0,Math.min(100,t/n*100))}function Ha(){let[t,n]=d(null),[s,l]=d([]),[a,o]=d(!1),[f,r]=d(""),[b,w]=d(""),[u,k]=d(""),[h,A]=d(""),[I,m]=d([]),[g,P]=d([]),[L,S]=d(!0),[N,p]=d(!1),[T,H]=d(""),[G,J]=d(null),[ne,x]=d({}),[R,F]=d(""),[W,Y]=d(!1),y=te(null),ae=ze(),E=le=>{n(le),o(!!le.enabled),r(le.cpus||""),w(le.mems||""),k(En(le.memory_max_bytes)),A(Fa(le.cpu_weight)),m(Array.isArray(le.io_limits)?le.io_limits:[]),P(Array.isArray(le.classes)?le.classes:[])},M=async()=>{S(!0);try{let[le,be]=await Promise.all([V("/api/cgroup-settings"),V("/api/host/block-devices").catch(()=>[])]);E(le),l(Array.isArray(be)?be:[]),H("")}catch(le){H(le instanceof Error?le.message:String(le))}finally{S(!1)}},se=async()=>{Y(!0);try{let le=await V("/api/cgroup-usage"),be=le.sampled_at?new Date(le.sampled_at).getTime():Date.now(),ve=Array.isArray(le.nodes)?le.nodes:[],K=y.current,$e={};if(K&&Number.isFinite(be)&&be>K.at){let O=(be-K.at)/1e3;for(let _e of ve){let Ee=_e.path||"",q=K.nodes[Ee];q&&($e[Ee]={cpuCores:Math.max(0,(_e.cpu_usage_usec-q.cpu)/1e6/O),read:Math.max(0,(_e.io_read_bytes-q.read)/O),write:Math.max(0,(_e.io_write_bytes-q.write)/O)})}}y.current={at:Number.isFinite(be)?be:Date.now(),nodes:Object.fromEntries(ve.map(O=>[O.path||"",{cpu:O.cpu_usage_usec,read:O.io_read_bytes,write:O.io_write_bytes}]))},x($e),J(le),F("")}catch(le){F(le instanceof Error?le.message:String(le))}finally{Y(!1)}};z(()=>{M()},[]);let ce=ae.segments[0]==="settings";z(()=>{if(!ce)return;se();let le=window.setInterval(()=>{document.hidden||se()},2e3);return()=>window.clearInterval(le)},[ce]);let v=async le=>{if(le.preventDefault(),!N){p(!0),H("");try{let be=await V("/api/cgroup-settings",{method:"PUT",body:JSON.stringify({enabled:a,cpus:f.trim(),mems:b.trim(),memory_max_bytes:Ra(u),cpu_weight:Ba(h),io_limits:I.map(ve=>({device:ve.device,read_bps:ve.read_bps||0,write_bps:ve.write_bps||0,read_iops:ve.read_iops||0,write_iops:ve.write_iops||0,weight:ve.weight||0})),classes:g.map(ve=>({name:ve.name.trim(),cpus:(ve.cpus||"").trim(),memory_max_bytes:ve.memory_max_bytes||0,cpu_weight:ve.cpu_weight||0,io_limits:(ve.io_limits||[]).map(K=>({device:K.device,read_bps:K.read_bps||0,write_bps:K.write_bps||0,read_iops:K.read_iops||0,write_iops:K.write_iops||0,weight:K.weight||0}))}))})});E(be),se(),oe(a?"Saved cgroup resource settings":"Cgroup limits disabled")}catch(be){H(be instanceof Error?be.message:String(be))}finally{p(!1)}}},$=Array.isArray(t?.online_cpus)?t.online_cpus:[],C=!!t?.available,U=!!t?.enabled,de=t?.reason||"",ie=U?t?.configured?`Applied at ${t?.root||"/sys/fs/cgroup/rookery-cgroup"}`:"Enabled but not applied yet":"Disabled",pe=Array.isArray(G?.nodes)?G.nodes:[],ee=pe.find(le=>!le.path)||null,ue=pe.filter(le=>{let be=le.path||"";return be?!pe.some(ve=>{let K=ve.path||"";return K!==""&&K.startsWith(`${be}/`)}):!1}),me=ue.length>0?ue:ee?[ee]:[],c=me.reduce((le,be)=>le+(ne[be.path||""]?.cpuCores||0),0),_=me.reduce((le,be)=>le+(ne[be.path||""]?.read||0),0),B=me.reduce((le,be)=>le+(ne[be.path||""]?.write||0),0),X=G?.online_cpus||$.length||0,Z=X>0?c/X*100:0,fe=ee?Ui(ee.memory_current_bytes,ee.memory_max_bytes):0,Ie=G?.sampled_at?`sampled ${new Date(G.sampled_at).toLocaleTimeString()}`:"waiting for sample";return e("section",{class:"panel panel-spaced","aria-labelledby":"cgroup-settings-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"cgroup-settings-heading",children:"Cgroup v2 resources"}),e("p",{class:"panel-subtitle",children:"Host-wide ceilings inherited by every QEMU process"})]}),e("button",{class:"button ghost",type:"button",disabled:L||N,onClick:()=>{M(),se()},children:L?"Refreshing\u2026":"Refresh"})]}),e("p",{class:"field-hint",children:["Rookery runs QEMU below ",t?.root||"/sys/fs/cgroup/rookery-cgroup",". VMs without a cgroup class run in the shared child cgroup; each class is a cgroup node below the",e("code",{children:"rookery-cgroup"})," root, for example ",e("code",{children:"rookery-cgroup/prod/web"}),"."]}),e("div",{class:"cgroup-status",children:[e("span",{class:"status-badge","data-status":C?U?t?.configured?"running":"paused":"disabled":"error",children:C?U?t?.configured?"applied":"not applied":"disabled":"unavailable"}),e("span",{class:"field-hint",children:[ie,t?.mount?` \xB7 mount ${t.mount}`:"",$.length>0?` \xB7 ${$.length} online CPUs`:"",g.length>0?` \xB7 ${g.length} classes`:"",t?.cpu_model?` \xB7 ${t.cpu_model}`:""]})]}),de?e("div",{class:"banner",role:"alert",children:de}):null,e("section",{class:"cgroup-telemetry","aria-labelledby":"cgroup-usage-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h3",{id:"cgroup-usage-heading",children:"Live cgroup usage"}),e("p",{class:"panel-subtitle",children:["Memory, CPU, task, and I/O telemetry from the running hierarchy \xB7 ",Ie]})]}),e("button",{class:"button ghost",type:"button",disabled:W,onClick:()=>{se()},children:W?"Sampling\u2026":"Refresh usage"})]}),R?e("div",{class:"banner",role:"alert",children:R}):G?G.available?G.configured?pe.length===0?e("p",{class:"field-hint",children:G.reason||"No cgroup nodes are available yet."}):e(ge,{children:[e("div",{class:"cgroup-metrics",children:[e("div",{class:"cgroup-metric",children:[e("span",{class:"field-label",children:"Memory"}),e("strong",{children:ee?_t(ee.memory_current_bytes):"\u2014"}),e("span",{class:"field-hint",children:ee&&ee.memory_max_bytes>0?`of ${_t(ee.memory_max_bytes)} \xB7 ${fe.toFixed(1)}%`:"unlimited"}),ee&&ee.memory_max_bytes>0?e("div",{class:"usage-bar",children:e("span",{style:{width:`${fe}%`}})}):null]}),e("div",{class:"cgroup-metric",children:[e("span",{class:"field-label",children:"CPU"}),e("strong",{children:[c.toFixed(2)," cores"]}),e("span",{class:"field-hint",children:X>0?`${Z.toFixed(1)}% of ${X} online CPUs`:"online CPU count unavailable"})]}),e("div",{class:"cgroup-metric",children:[e("span",{class:"field-label",children:"Tasks"}),e("strong",{children:ee?.pids_current??"\u2014"}),e("span",{class:"field-hint",children:ee&&ee.pids_max>0?`of ${ee.pids_max}`:"no task limit"})]}),e("div",{class:"cgroup-metric",children:[e("span",{class:"field-label",children:"I/O"}),e("strong",{children:qt(_)}),e("span",{class:"field-hint",children:["read \xB7 ",qt(B)," write"]})]})]}),e("div",{class:"table-wrap",children:e("table",{class:"cgroup-usage-table",children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Scope"}),e("th",{scope:"col",children:"Memory"}),e("th",{scope:"col",children:"CPU"}),e("th",{scope:"col",children:"Tasks"}),e("th",{scope:"col",children:"I/O"})]})}),e("tbody",{children:pe.map(le=>{let be=le.path||"",ve=ne[be];return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name mono",children:le.name||be||"rookery-cgroup"}),be?e("span",{class:"vm-description mono",children:be}):null]}),e("td",{class:"mono",children:Oi(le)}),e("td",{class:"mono",children:ve?`${ve.cpuCores.toFixed(2)} cores`:"\u2014"}),e("td",{class:"mono",children:[le.pids_current,le.pids_max>0?` / ${le.pids_max}`:""]}),e("td",{class:"mono",children:ve?`\u2193 ${qt(ve.read)} \xB7 \u2191 ${qt(ve.write)}`:"\u2014"})]},be||"root")})})]})})]}):e("p",{class:"field-hint",children:G.reason||"Enable cgroup limits to start collecting resource usage."}):e("p",{class:"field-hint",children:"cgroup v2 is not available on this host."}):e("p",{class:"field-hint",children:"Waiting for the first cgroup sample\u2026"})]}),e("form",{class:"form-grid",onSubmit:le=>{v(le)},children:[e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:a,onChange:le=>o(le.currentTarget.checked)}),e("span",{children:"Enable cgroup v2 limits for QEMU"})]}),e("span",{class:"field-hint",children:"Saving applies the new host defaults to every running VM immediately: Rookery re-places each live QEMU process in its shared or class cgroup. New and restarted VMs are always placed under this hierarchy. When disabled, running QEMU processes are moved out immediately and the Rookery cgroups are removed; all VMs then run unconstrained, and class limits take effect again when host limits are enabled. Shrinking the host CPU set clamps running VMs to the remaining CPUs."})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"CPU cores"}),e(Tn,{value:f,options:$,disabled:!a,onChange:r,hint:"Click cores to pin QEMU to them. The text field accepts ranges. Empty means all online CPUs."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Total memory limit (MB)"}),e("input",{type:"number",min:0,step:128,disabled:!a,value:u,placeholder:"unlimited",onInput:le=>k(le.currentTarget.value)}),e("span",{class:"field-hint",children:"memory.max for the whole Rookery cgroup subtree. Empty means unlimited."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"CPU weight"}),e("input",{type:"number",min:0,max:1e4,step:1,disabled:!a,value:h,placeholder:"default (100)",onInput:le=>A(le.currentTarget.value)}),e("span",{class:"field-hint",children:"cpu.weight (1\u201310000). Empty uses the kernel default of 100."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Memory nodes ",e("em",{children:"optional"})]}),e("input",{type:"text",disabled:!a,value:b,placeholder:"e.g. 0-1 (empty means all)",onInput:le=>w(le.currentTarget.value)}),e("span",{class:"field-hint",children:"cpuset.mems NUMA node list."})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Per-device I/O limits"}),e(Pn,{limits:I,devices:s,disabled:!a,onChange:m}),s.length===0?e("span",{class:"field-hint",children:["No whole block devices were reported by the host. Check access to ",e("code",{children:"/sys/class/block"}),"."]}):null,e("span",{class:"field-hint",children:"io.max limits read/write bandwidth (MiB/s) and IOPS. io.weight sets the proportional weight for that device (1\u201310000; requires the BFQ scheduler)."})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Classes"}),e("span",{class:"field-hint",children:["Define a tree of reusable profiles below ",e("code",{children:"rookery-cgroup"}),". Class paths are slash-separated and may be up to 3 levels deep, for example ",e("code",{children:"prod"}),","," ",e("code",{children:"prod/web"}),", or ",e("code",{children:"prod/web/frontend"}),". Define the parent class first. VMs can only be assigned to leaf classes; parent classes group their children and inherit limits. Assign a class to a VM instead of entering limits per VM. Class limits are shared by every VM assigned to that class; allow enough memory for all of them."]}),e(Ki,{classes:g,devices:s,onlineCPUs:$,disabled:L||N,onChange:P})]}),T?e("p",{class:"form-error field-wide",role:"alert",children:T}):null,e("div",{class:"field-wide actions",children:e("button",{class:"button primary",type:"submit",disabled:N||L,children:N?"Saving\u2026":"Save cgroup settings"})})]})]})}function qi(t,n){let s=1;for(;;){let l=n?`${n}/class-${s}`:`class-${s}`;if(!t.some(a=>a.name===l))return l;s++}}function Oa(t){let n=t.lastIndexOf("/");return n>0?t.slice(0,n):""}function Wi(t){let n=t.lastIndexOf("/");return n>=0?t.slice(n+1):t}function zi(t,n,s){let l=s.trim();if(!l||l.includes("/"))return null;let a=Oa(n),o=a?`${a}/${l}`:l;return o===n?t:t.some(f=>f.name===o)?null:t.map(f=>f.name===n?{...f,name:o}:f.name.startsWith(n+"/")?{...f,name:o+f.name.slice(n.length)}:f)}function Gi(t){let n=[];return t.cpus&&n.push(`CPU ${t.cpus}`),t.memory_max_bytes&&n.push(`${En(t.memory_max_bytes)} MB`),t.cpu_weight&&n.push(`weight ${t.cpu_weight}`),t.io_limits?.length&&n.push(`${t.io_limits.length} I/O`),n.length?n.join(" \xB7 "):"inherits host limits"}function Ki({classes:t,devices:n,onlineCPUs:s,disabled:l,onChange:a}){let o=(h,A)=>{a(t.map(I=>I.name===h?{...I,...A}:I))},f=h=>{a(t.filter(A=>A.name!==h&&!A.name.startsWith(h+"/")))},r=h=>{a([...t,{name:qi(t,h)}])},b=new Map(t.map(h=>[h.name,h])),w=new Map,u=[];for(let h of t){let A=Oa(h.name);if(A&&b.has(A)){let I=w.get(A)||[];I.push(h),w.set(A,I)}else u.push(h)}for(let h of w.values())h.sort((A,I)=>A.name.localeCompare(I.name));u.sort((h,A)=>h.name.localeCompare(A.name));let k=h=>{let A=h.name.split("/").length,I=w.get(h.name)||[],m=Wi(h.name);return e("div",{class:"cgroup-tree-node",children:[e("div",{class:"cgroup-tree-node-head",children:[e("div",{class:"cgroup-tree-title",children:[e("input",{class:"cgroup-tree-name",type:"text",defaultValue:m,disabled:l,"aria-label":`Class name for ${h.name}`,onBlur:g=>{let P=zi(t,h.name,g.currentTarget.value);if(!P){g.currentTarget.value=m;return}a(P)}}),e("span",{class:"cgroup-tree-path",children:h.name}),e("span",{class:"field-hint",children:Gi(h)})]}),e("div",{class:"actions",children:[A<3?e("button",{class:"row-action",type:"button",disabled:l||t.length>=64,onClick:()=>r(h.name),children:"Add child"}):null,e("button",{class:"row-action danger",type:"button",disabled:l||I.length>0,title:I.length>0?"Remove child classes first.":"",onClick:()=>f(h.name),children:"Remove"})]})]}),e("details",{class:"cgroup-tree-limits",children:[e("summary",{children:"Limits"}),e("div",{class:"form-grid cgroup-class-grid",children:[e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"CPU cores"}),e(Tn,{value:h.cpus||"",options:s,disabled:l,onChange:g=>o(h.name,{cpus:g}),hint:"Empty inherits the host/parent class CPU set. A non-empty set must be a subset of it."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Memory limit (MB)"}),e("input",{type:"number",min:0,step:128,disabled:l,value:En(h.memory_max_bytes),placeholder:"inherit",onInput:g=>o(h.name,{memory_max_bytes:Ra(g.currentTarget.value)})})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"CPU weight"}),e("input",{type:"number",min:0,max:1e4,step:1,disabled:l,value:Fa(h.cpu_weight),placeholder:"inherit",onInput:g=>o(h.name,{cpu_weight:Ba(g.currentTarget.value)})})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Per-device I/O limits"}),e(Pn,{limits:h.io_limits||[],devices:n,disabled:l,onChange:g=>o(h.name,{io_limits:g})})]})]})]}),I.length>0?e("div",{class:"cgroup-tree-children",children:I.map(k)}):null]},h.name)};return e("div",{class:"cgroup-tree",children:[e("div",{class:"cgroup-tree-root",children:[e("div",{children:[e("span",{class:"cgroup-tree-root-name",children:"rookery-cgroup"}),e("span",{class:"field-hint",children:"Root limits are the host settings above; classes nest up to 3 levels below."})]}),e("button",{class:"button ghost",type:"button",disabled:l||t.length>=64,onClick:()=>r(),children:"Add top-level class"})]}),u.length>0?u.map(k):e("p",{class:"field-hint",children:"No classes defined. VMs without a class run in the shared cgroup."})]})}function Ji(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function Xi(t){switch(t){case"success":return"running";case"failed":return"error";case"running":return"starting";case"pending":return"paused";default:return"disabled"}}function Ua({kind:t,title:n,endpoint:s,description:l,placeholder:a,spaced:o}){let[f,r]=d(null),[b,w]=d(!1),[u,k]=d(""),[h,A]=d(!0),[I,m]=d(!1),[g,P]=d(""),L=t==="rookery"?"Rookery":"VPP",S=`${t}-startup-script-heading`,N=async()=>{A(!0);try{let J=await V(s);r(J),w(!!J.enabled),k(J.script||""),P("")}catch(J){P(J instanceof Error?J.message:String(J))}finally{A(!1)}};z(()=>{N()},[]);let p=async J=>{if(J.preventDefault(),!I){if(b&&!u.trim()){P(`Enter a ${L} script before enabling it.`);return}m(!0),P("");try{let ne=await V(s,{method:"PUT",body:JSON.stringify({enabled:b,script:u})});r(ne),w(!!ne.enabled),k(ne.script||""),oe(ne.enabled?`Saved ${L} startup script${t==="rookery"?"; it will run on the next API start":"; it will run on the next VPP connection"}`:`Saved ${L} startup script settings`)}catch(ne){P(ne instanceof Error?ne.message:String(ne))}finally{m(!1)}}},T=f?.status||"disabled",H=f?.output||"",G=T==="success"?0:f?.exit_code??"\u2014";return e("section",{class:o?"panel panel-spaced":"panel","aria-labelledby":S,children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:S,children:n}),e("p",{class:"panel-subtitle",children:t==="rookery"?"Rookery server lifecycle hook":"Applied whenever Rookery connects to FD.io VPP"})]}),e("button",{class:"button ghost",type:"button",disabled:h||I,onClick:()=>{N()},children:h?"Refreshing\u2026":"Refresh"})]}),e("p",{class:"field-hint",children:l}),e("div",{class:"startup-script-status",children:[e("span",{class:"status-badge","data-status":Xi(T),children:T}),T==="success"||T==="failed"?e("span",{class:"field-hint",children:["exit code ",G,f?.ran_at?` \xB7 ${Ji(f.ran_at)}`:""]}):T==="running"?e("span",{class:"field-hint",children:"Running\u2026"}):b?e("span",{class:"field-hint",children:t==="rookery"?"Runs on the next API start.":"Runs on the next VPP connection."}):e("span",{class:"field-hint",children:"Disabled."})]}),e("form",{class:"form-grid",onSubmit:J=>{p(J)},children:[e("label",{class:"field field-wide checkbox-field",children:e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:b,onChange:J=>w(J.currentTarget.checked)}),e("span",{children:["Enable ",L," startup script"]})]})}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Script"}),e("textarea",{class:"startup-script-editor",rows:t==="rookery"?7:9,spellcheck:!1,placeholder:a,value:u,onInput:J=>k(J.currentTarget.value)}),e("span",{class:"field-hint",children:"The script runs with `/bin/sh` and the API process privileges. Only configure trusted commands."})]}),g?e("p",{class:"form-error field-wide",role:"alert",children:g}):null,e("div",{class:"field-wide actions",children:e("button",{class:"button primary",type:"submit",disabled:I||h,children:I?"Saving\u2026":`Save ${L} startup script`})})]}),H?e("details",{class:"startup-script-output",open:T==="failed",children:[e("summary",{children:"Last output"}),e("pre",{children:H})]}):null]})}function qa(){return e(ge,{children:[e(Ha,{}),e(Ua,{kind:"rookery",title:"Rookery startup script",endpoint:"/api/startup-script",description:"Runs once per API process after Rookery starts. It is empty by default. Use it for host-level setup that should happen once when the service starts.",placeholder:`# Runs once when the Rookery API starts -# Add host or setup commands here`,spaced:!0}),e(Ua,{kind:"vpp",title:"VPP startup script",endpoint:"/api/vpp-startup-script",description:"Runs every time Rookery connects to FD.io VPP, including after the connection drops or VPP restarts. It is enabled by default to assign NAT44 workers.",placeholder:`# Assign NAT44 workers -vppctl set nat workers 0`,spaced:!0})]})}function Wa(t){t&&Fe(e(qa,{}),t)}function Wt(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}var Zi="[A-Za-z0-9][A-Za-z0-9._\\-]*";function In(t,n){if(!t)return"";let[s,l]=t.trim().split("/"),a=s.split(".").map(Number);if(a.length!==4||a.some(f=>!Number.isInteger(f)||f<0||f>255)||!l)return"";let o=(a[0]<<24>>>0)+(a[1]<<16)+(a[2]<<8)+a[3]+n>>>0;return`${o>>>24&255}.${o>>>16&255}.${o>>>8&255}.${o&255}`}function ji(t,n){if(!t)return"";let[s,l]=t.trim().split("/");if(!s||!l)return"";let a=s.split("::");if(a.length>2)return"";let o=h=>h?h.split(":"):[],f=o(a[0]),r=o(a[1]??""),b=8-f.length-r.length;if(b<0)return"";let w=[...f,...Array.from({length:b},()=>"0"),...r];if(w.some(h=>!/^[0-9a-fA-F]{1,4}$/.test(h)))return"";let u=0n;for(let h of w)u=(u<<16n)+BigInt(parseInt(h,16));u+=BigInt(n);let k=[];for(let h=0;h<8;h+=1)k.unshift((u&0xffffn).toString(16)),u>>=16n;return k.join(":")}function An(t){let n=In(t,1);return!n||!t?"":`${n}/${t.trim().split("/")[1]}`}function Yi(t,n,s,l){let a=t?.type??"dhcpv4",o=t?.config??{},f=n.find(b=>b.id===(t?.network_id??l??n[0]?.id)),r=An(f?.ipv4_cidr)||"10.0.0.1/16";return{name:t?.name??"",networkID:t?.network_id??l??n[0]?.id??0,type:a,enabled:t?t.enabled:s,gateway:o.gateway??(a==="dhcpv6"?"fd00:20::1/64":r),dns:o.dns??(a==="dhcpv6"?"2001:4860:4860::8888":"1.1.1.1"),rangeStart:o.range_start??(a==="dhcpv6"?"fd00:20::100":"10.20.0.100"),rangeEnd:o.range_end??(a==="dhcpv6"?"fd00:20::200":"10.20.0.200"),leaseTime:o.lease_time??"12h",outsideInterface:o.outside_interface??"",outsideAddress:o.outside_address??"",outsideGateway:o.outside_gateway??"",nat44Pool:o.nat44_pool??"",dnatRules:Array.isArray(o.dnat_rules)?o.dnat_rules.map(b=>({...b})):[],staticAssignments:Array.isArray(o.static_assignments)?o.static_assignments.map(b=>({...b})):[],dhcpOptions:Array.isArray(o.dhcp_options)?o.dhcp_options.map(b=>({...b})):[],dnsRecords:Array.isArray(o.dns_records)?o.dns_records.map(b=>({...b})):[],dnsForward:o.dns_forward??!1,dnsForwarders:Array.isArray(o.dns_forwarders)?o.dns_forwarders.join(", "):t?.type==="dns"?"":"1.1.1.1, 8.8.8.8",dnsVNFID:o.dns_vnf_id??0,dnsDomain:o.dns_domain??"",dnsTTL:String(o.dns_ttl??300),transport:a==="bgp"?o.transport==="tap"?"tap":"memif":o.transport==="shared"||o.transport==="dedicated"||o.transport==="tap"?o.transport:"inherit",localAS:String(o.local_as??64512),routerID:o.router_id??"10.20.0.1",peerAS:String(o.peer_as??64513),peerIP:o.peer_ip??"169.254.0.2",localIP:o.local_ip??"169.254.0.1/30",peerGateway:o.peer_gateway??"",nextHop:o.next_hop??"",multihopTTL:String(o.multihop_ttl??1),password:o.password??"",passive:o.passive??!1,targetTableID:String(o.target_table_id??0),advertisePrefixes:Array.isArray(o.advertise_prefixes)?o.advertise_prefixes.join(` -`):""}}function Qi(t,n){return n==="dns"?{...t,type:n,dnsRecords:[],dnsForward:!0,dnsForwarders:"1.1.1.1, 8.8.8.8"}:n==="bgp"?{...t,type:n,transport:"memif",localAS:"64512",routerID:"10.20.0.1",peerAS:"64513",peerIP:"169.254.0.2",localIP:"169.254.0.1/30",peerGateway:"",nextHop:"",multihopTTL:"1",password:"",passive:!1,targetTableID:"0",advertisePrefixes:""}:n==="dhcpv6"?{...t,type:n,transport:"tap",gateway:"fd00:20::1/64",dns:"2001:4860:4860::8888",rangeStart:"fd00:20::100",rangeEnd:"fd00:20::200",leaseTime:"12h",dnsVNFID:0,dnsDomain:"",dnsTTL:"300"}:n==="nat44"?{...t,type:n,gateway:"10.20.0.1/24",outsideAddress:"",outsideGateway:"",nat44Pool:""}:{...t,type:n,transport:"inherit",gateway:"10.20.0.1/24",dns:"1.1.1.1",rangeStart:"10.20.0.100",rangeEnd:"10.20.0.200",leaseTime:"12h",dnsVNFID:0,dnsDomain:"",dnsTTL:"300"}}function er(t,n){let s=t.find(l=>l.name===n);return s?.default_route?"This NIC carries the host default route and cannot be used for NAT.":s?.physical===!1?"Select a wired physical NIC; virtual and VPP-managed interfaces are not valid outside uplinks.":s?.wireless?"Wireless NICs are not supported as VPP NAT44 outside uplinks; use a wired NIC.":s?`VPP will take over ${s.name} and flush its host addresses.`:"The selected NIC will be taken over by VPP; its host addresses are flushed."}function tr(t){return t==="dns"?"Local DNS server on the network service address (.2) over IPv4 and IPv6. Serve A/AAAA/CNAME/TXT/PTR records and optionally forward other names upstream.":t==="bgp"?"Embedded IPv4/IPv6 BGP speaker. Select memif for userspace/gVisor or tap for kernel TCP with TCP MD5 support.":t==="nat44"?"IPv4 NAT44 gateway. A dedicated network BVI owns the gateway; the outside NIC is taken over by VPP.":t==="dhcpv6"?"IPv6 address pool with stateful IA_NA leases on a VPP tap (shared memif is DHCPv4-only for now).":"IPv4 address pool served on the network-shared memif by default (VNF address .2)."}var nr=[{code:3,label:"Router",format:"ipv4",placeholder:"10.20.0.1",hint:"Overrides the gateway router option."},{code:6,label:"DNS servers",format:"ipv4",placeholder:"10.20.0.2, 1.1.1.1",hint:"Overrides the DNS field."},{code:15,label:"Domain name",format:"string",placeholder:"lab.test",hint:"This is the domain name, not DNS server addresses. Use code 6 for DNS servers."},{code:42,label:"NTP servers",format:"ipv4",placeholder:"10.20.0.1, 10.20.0.2"},{code:44,label:"WINS servers",format:"ipv4",placeholder:"10.20.0.10"},{code:66,label:"TFTP server name",format:"string",placeholder:"tftp.lab.test"},{code:67,label:"Bootfile name",format:"string",placeholder:"pxelinux.0"},{code:119,label:"Domain search list",format:"domain-list",placeholder:"lab.test, apps.lab.test"},{code:252,label:"WPAD URL",format:"string",placeholder:"http://wpad.lab.test/wpad.dat"}],sr=[{code:21,label:"SIP domain names",format:"domain-list",placeholder:"sip.lab.test"},{code:22,label:"SIP servers",format:"ipv6",placeholder:"fd00:20::10"},{code:23,label:"DNS recursive name servers",format:"ipv6",placeholder:"fd00:20::2",hint:"Overrides the DNS field."},{code:24,label:"Domain search list",format:"domain-list",placeholder:"lab.test, apps.lab.test"},{code:31,label:"SNTP servers",format:"ipv6",placeholder:"fd00:20::10"},{code:59,label:"Bootfile URL",format:"string",placeholder:"http://[fd00:20::1]/boot"},{code:60,label:"Bootfile parameters",format:"string",placeholder:"console=ttyS0"}];function za(t){return t==="dhcpv6"?sr:nr}function Ln(t,n){return za(t).find(s=>s.code===n)}function Ga({vnf:t,networks:n,vnfs:s,vppReady:l,initialNetworkID:a,onClose:o,onSaved:f}){let[r,b]=d(()=>Yi(t,n,l,a)),[w,u]=d([]),[k,h]=d(!1),[A,I]=d(!1),[m,g]=d(""),[P,L]=ke("vnf",t?.id),S=te(null),N=te(null),p=t!==null,T=r.type==="nat44",H=r.type==="bgp",G=r.type==="dns",J=n.find(c=>c.id===r.networkID),ne=In(J?.ipv4_cidr,2),x=ji(J?.ipv6_cidr,2),R=s.filter(c=>c.type==="dns"&&c.network_id===r.networkID),F=(()=>{let c=J?.ipv4_cidr?.trim(),_=In(c,2),B=c?.split("/")[1];return _&&B?`${_}/${B}`:""})(),W=r.gateway||"the gateway address";z(()=>{S.current?.focus(),document.title=`${t?"Edit":"Add"} VNF \xB7 rookery`},[t]),z(()=>{if(!T)return;let c=!1;return h(!0),V("/api/host/interfaces").then(_=>{c||u(Array.isArray(_)?_:[])}).catch(()=>{c||u([])}).finally(()=>{c||h(!1)}),()=>{c=!0}},[T]);let Y=(c,_)=>{b(B=>({...B,[c]:_}))},y=()=>{A||o()},ae=async c=>{if(c.preventDefault(),A||!N.current?.reportValidity())return;if(g(""),!H&&!G&&!r.gateway.trim()){g("Gateway CIDR is required.");return}if(!H&&!T&&!G&&(!r.rangeStart.trim()||!r.rangeEnd.trim())){g("Range start and end are required.");return}if(T&&!r.outsideInterface){g("Select an outside network interface.");return}if(T&&r.enabled&&!window.confirm(`Take over ${r.outsideInterface} for NAT44? +This removes the VPP vhost-user interface and socket.`))try{await V(`/api/interfaces/${i.id}`,{method:"DELETE"}),Be({interfaces:b.filter(Y=>Y.id!==i.id)}),M(""),ae(`Deleted interface ${i.name}`)}catch(Y){M(`Could not delete ${i.name}: ${Y instanceof Error?Y.message:String(Y)}`)}},To=async i=>{let D=i.attached_vm_id?h.find(Y=>Y.id===i.attached_vm_id):null;if(D&&(D.status==="running"||D.status==="starting")){ae("Stop the VM before detaching its interface.",!0);return}if(window.confirm(`Detach interface "${i.name}" from its VM?`))try{let Y=await V(`/api/interfaces/${i.id}`,{method:"PUT",body:JSON.stringify({attached_vm_id:null})});Be({interfaces:b.map(ce=>ce.id===Y.id?Y:ce)}),M(""),ae(`Detached ${i.name}`)}catch(Y){M(`Could not detach ${i.name}: ${Y instanceof Error?Y.message:String(Y)}`)}},Po=async i=>{if(Bt(i,b).length>0){ae(`Remove ACL ${i.name} from its interfaces first.`,!0);return}if(window.confirm(`Delete ACL "${i.name}"?`))try{await V(`/api/acls/${i.id}`,{method:"DELETE"}),Be({acls:d.filter(D=>D.id!==i.id)}),O(""),ae(`Deleted ACL ${i.name}`)}catch(D){O(`Could not delete ${i.name}: ${D instanceof Error?D.message:String(D)}`),ae(`Could not delete ACL ${i.name}: ${D instanceof Error?D.message:String(D)}`,!0)}},Eo=async i=>{if(!i.is_default&&window.confirm(`Delete route table "${i.name}" (${i.table_id}/${i.ipv6?"IPv6":"IPv4"})?`))try{await V(`/api/route-tables/${i.id}`,{method:"DELETE"}),await dt(),ae(`Deleted route table ${i.name}`)}catch(D){let Y=D instanceof Error?D.message:String(D);_e(`Could not delete ${i.name}: ${Y}`),ae(`Could not delete route table ${i.name}: ${Y}`,!0)}},Lo=i=>{window.dispatchEvent(new CustomEvent("rookery:open-tab",{detail:"vnfs"})),window.dispatchEvent(new CustomEvent("rookery:add-vnf",{detail:{networkId:i.id}}))},Io=b.filter(i=>i.attached_vm_id||i.attached_lightvm_id).length,Ao=d.filter(i=>Bt(i,b).length>0).length,No=t.length===0?L?"Loading networks\u2026":"No Rookery Networks registered":`${t.length} network${t.length===1?"":"s"} \xB7 ${b.length} interface${b.length===1?"":"s"} \xB7 ${I.length} VNF${I.length===1?"":"s"}`,Do=s.length===0?L?"Loading host interfaces\u2026":"No host interfaces found":`${s.length} host interface${s.length===1?"":"s"} \xB7 ${s.filter(i=>i.in_vpp).length} in VPP`,xo=f.length===0?L?"Loading host taps\u2026":"No network host taps":`${f.length} host tap${f.length===1?"":"s"} \xB7 ${new Set(f.map(i=>i.network_id)).size} network${new Set(f.map(i=>i.network_id)).size===1?"":"s"}`,Vo=b.length===0?L?"Loading interfaces\u2026":"No virtual interfaces registered":`${b.length} interface${b.length===1?"":"s"} \xB7 ${Io} attached`,Mo=d.length===0?L?"Loading ACLs\u2026":"No ACLs defined":`${d.length} ACL${d.length===1?"":"s"} \xB7 ${Ao} assigned`,Yn=i=>!i||!i.acl_indexes||i.acl_indexes.length===0?"no ACL":`${Ye(i.matched)} matched \xB7 ${Ye(i.denied)} denied \xB7 ${Ye(i.permitted)} permitted`,$o=i=>{let D=i.acl_index??0,Y=i.rule_index??0,be=d.find(He=>He.vpp_index===D)?.rules[Y];return be?vn(be):`ACL ${D} rule ${Y}`},Qn=(i,D)=>{let Y=D?.rules||[];return e("div",{class:"acl-rule-counter-group",children:[e("span",{class:"acl-counter-direction",children:i}),!D?.acl_indexes||D.acl_indexes.length===0?e("span",{class:"acl-rule-counter muted",children:"No ACL applied"}):Y.length===0?e("span",{class:"acl-rule-counter muted",children:"No explicit rule matches yet"}):Y.map((ce,be)=>e("span",{class:"acl-rule-counter",children:[e("span",{class:"acl-rule-counter-text",children:$o(ce)}),e("span",{class:"acl-rule-counter-value",children:[Ye(ce.matched)," pkts \xB7 ",ce.action||"\u2014"]})]},`${ce.acl_index??0}-${ce.rule_index??0}-${be}`))]})};return e(ge,{children:[e("div",{class:"subtabs",role:"tablist","aria-label":"Network sections",children:As.map(i=>e("button",{class:"subtab",type:"button",role:"tab","aria-selected":Ie===i.id,onClick:()=>{te(`/networks/${i.id}`),i.id==="interfaces"&&en()},children:i.label},i.id))}),e("section",{class:"panel","aria-labelledby":"networks-heading",hidden:Ie!=="networks",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"networks-heading",children:"Network"}),e("p",{class:"panel-subtitle",children:No})]}),e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"})]}),N?e("div",{class:"banner",role:"alert",children:N}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Bridge domain"}),e("th",{scope:"col",children:"Interfaces"}),e("th",{scope:"col",children:"VNFs"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(i=>{let D=b.filter(ce=>ce.network_id===i.id).length,Y=I.filter(ce=>ce.network_id===i.id);return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:i.name}),e("span",{class:"vm-description mono",children:[i.ipv4_cidr||"no IPv4 CIDR"," \xB7 ",i.ipv6_cidr||"no IPv6 CIDR"]}),i.bvi_host_name?e("span",{class:"vm-description",children:[i.bvi_host_name,i.ipv4_gateway?` \xB7 ${i.ipv4_gateway}`:""]}):null,e(De,{entityType:"network",entityID:i.id,name:i.name,labels:Hn.labels[String(i.id)],onSaved:ce=>Hn.setEntityLabels(String(i.id),ce)})]}),e("td",{class:"mono",children:["bd ",i.bridge_domain_id]}),e("td",{class:D?"mono":"mono muted",children:String(D)}),e("td",{class:Y.length?"mono":"mono muted",children:Y.length?Y.map(ce=>`${ce.name} (${ce.type})`).join(", "):"none"}),e("td",{class:"updated hide-sm",children:pt(i.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>te(`/networks/${i.id}/edit`),children:"Edit CIDRs"}),e("button",{class:"row-action",type:"button",onClick:()=>Lo(i),children:"Add VNF"}),e("button",{class:"row-action danger",type:"button",disabled:D>0||!y,title:D>0?"Delete the virtual interfaces first.":y?"":"VPP must be ready to delete a bridge domain.",onClick:()=>{ko(i)},children:"Delete"})]})})]},i.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2B22"}),e("h3",{children:"No Networks yet"}),e("p",{children:"Create a VPP bridge domain to connect virtual interfaces and VMs."}),e("button",{class:"button primary",type:"button",disabled:!y,onClick:()=>te("/networks/new"),children:"Create a network"})]})})]}),e("section",{class:"panel","aria-labelledby":"pipes-heading",hidden:Ie!=="pipes",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"pipes-heading",children:"Bridge-domain pipes"}),e("p",{class:"panel-subtitle",children:"Connect two Rookery Networks with a VPP pipe pair."})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",disabled:!y||t.length<2,title:t.length<2?"Create at least two networks first":y?"":"VPP must be ready",onClick:()=>x(!0),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add pipe"]})]})]}),e("p",{class:"field-hint",children:"A pipe creates two VPP interfaces joined back-to-back. One end is placed in each bridge domain, providing an L2 bridge between the selected networks."}),t.length<2?e("p",{class:"field-hint",children:"Create at least two networks before adding a pipe."}):null,T?e("div",{class:"banner",role:"alert",children:T}):null,e("div",{class:"table-wrap",children:a.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Network A"}),e("th",{scope:"col",children:"Network B"}),e("th",{scope:"col",children:"VPP instance"}),e("th",{scope:"col",children:"Endpoints"}),e("th",{scope:"col",class:"hide-sm",children:"Created"}),e("th",{scope:"col",children:"Actions"})]})}),e("tbody",{children:a.map(i=>e("tr",{children:[e("td",{children:e("span",{class:"vm-name",children:i.network_a_name})}),e("td",{children:[e("span",{class:"vm-name",children:i.network_b_name}),e(De,{entityType:"network_pipe",entityID:i.id,name:`${i.network_a_name} \u2194 ${i.network_b_name}`,labels:On.labels[String(i.id)],onSaved:D=>On.setEntityLabels(String(i.id),D)})]}),e("td",{class:"mono",children:["pipe",i.instance]}),e("td",{class:"mono",children:[e("span",{children:[Ot(i,0)," (",i.endpoint_a_sw_if_index||"\u2014",")"]}),e("span",{children:" \u2194 "}),e("span",{children:[Ot(i,1)," (",i.endpoint_b_sw_if_index||"\u2014",")"]})]}),e("td",{class:"updated hide-sm",children:pt(i.created_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:!y||!i.endpoint_a_sw_if_index,onClick:()=>j({name:Ot(i,0),label:`pipe endpoint A \xB7 ${i.network_a_name}`}),children:"A addresses"}),e("button",{class:"button ghost",type:"button",disabled:!y||!i.endpoint_b_sw_if_index,onClick:()=>j({name:Ot(i,1),label:`pipe endpoint B \xB7 ${i.network_b_name}`}),children:"B addresses"}),e("button",{class:"row-action danger",type:"button",disabled:K===String(i.id)||!y,onClick:()=>{Co(i)},children:K===String(i.id)?"Deleting\u2026":"Delete"})]})})]},i.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2194"}),e("h3",{children:"No bridge-domain pipes"}),e("p",{children:"Connect two Rookery Networks to pass L2 traffic between their bridge domains."}),e("button",{class:"button primary",type:"button",disabled:!y||t.length<2,onClick:()=>x(!0),children:"Add pipe"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"hosts-heading",hidden:Ie!=="hosts",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"hosts-heading",children:"Host interfaces"}),e("p",{class:"panel-subtitle",children:Do})]}),e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"})]}),e("p",{class:"field-hint",children:["Physical NICs and external interfaces visible to the server; Rookery/VPP taps and host interfaces are hidden. \u201CIn VPP\u201D means a matching",e("code",{children:"host-"})," interface currently exists in VPP; the VPP state is shown as unknown when VPP cannot be queried. Attach takes a physical NIC into VPP (flushing its host addresses); persisted attachments are recreated after a VPP restart."]}),R?e("div",{class:"banner",role:"alert",children:R}):null,e("div",{class:"table-wrap",children:s.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Interface"}),e("th",{scope:"col",children:"Host / VPP addresses"}),e("th",{scope:"col",children:"Link"}),e("th",{scope:"col",children:"VPP"}),e("th",{scope:"col",children:"Default route"}),e("th",{scope:"col",children:"Type"}),e("th",{scope:"col",children:"Actions"})]})}),e("tbody",{children:s.map(i=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name mono",children:i.name}),e(De,{entityType:"host_interface",entityID:i.name,name:i.name,labels:Un.labels[i.name],onSaved:D=>Un.setEntityLabels(i.name,D)})]}),e("td",{class:"mono",children:e("div",{class:"address-stack",children:[e("span",{children:[e("span",{class:"address-side",children:"Host"}),i.addresses&&i.addresses.length>0?i.addresses.join(", "):"\u2014"]}),i.in_vpp?e("span",{children:[e("span",{class:"address-side",children:"VPP"}),i.vpp_addresses&&i.vpp_addresses.length>0?i.vpp_addresses.join(", "):"\u2014"]}):null]})}),e("td",{children:e("span",{class:"status-badge","data-status":i.up?"running":"disabled",children:i.up?"up":"down"})}),e("td",{children:i.vpp_checked?i.in_vpp?e(ge,{children:[e("span",{class:"status-badge","data-status":i.vpp_up?"running":"degraded",children:"in VPP"}),e("span",{class:"vm-description mono",children:[i.vpp_name||`host-${i.name}`,typeof i.vpp_sw_if_index=="number"?` \xB7 sw_if_index ${i.vpp_sw_if_index}`:"",i.managed?" \xB7 persisted":""]})]}):e("span",{class:"status-badge","data-status":"disabled",children:"not in VPP"}):e("span",{class:"status-badge","data-status":"disabled",children:"unknown"})}),e("td",{class:"mono",children:i.default_route?"yes":"\u2014"}),e("td",{children:i.wireless?"wireless":i.physical?"physical":"virtual"}),e("td",{children:e("div",{class:"actions",children:i.in_vpp?e(ge,{children:[i.managed?e(ge,{children:[e("button",{class:"button ghost",type:"button",disabled:!y,onClick:()=>j({name:i.vpp_name||`host-${i.name}`,label:"host interface",hostAddresses:i.addresses||[]}),children:"Addresses"}),e("button",{class:"button ghost",type:"button",disabled:!y,onClick:()=>pe({name:i.vpp_name||`host-${i.name}`,label:"Host interface"}),children:"Route tables"})]}):null,e("button",{class:"button ghost",type:"button",disabled:re===i.name||!y,onClick:()=>{vo(i)},children:re===i.name?"Detaching\u2026":"Detach"})]}):e("button",{class:"button ghost",type:"button",disabled:re===i.name||!y||!i.physical||i.wireless||i.default_route,title:i.default_route?"The host default-route interface cannot be attached":i.wireless?"Wireless interfaces cannot be attached":i.physical?"Take this NIC into VPP":"Only physical interfaces can be attached",onClick:()=>{go(i)},children:re===i.name?"Attaching\u2026":"Attach to VPP"})})})]},i.name))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u21C4"}),e("h3",{children:"No host interfaces found"}),e("p",{children:"Linux network interfaces will appear here when available."})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"hosttaps-heading",hidden:Ie!=="hosttaps",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"hosttaps-heading",children:"Host taps"}),e("p",{class:"panel-subtitle",children:xo})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",disabled:!y||t.length===0,onClick:()=>z(!0),children:"Add host tap"})]})]}),e("p",{class:"field-hint",children:"Host taps bridge a Rookery Network into a Linux tap interface on the server. Assign a host IPv4 address here (or use DHCP on the network) and the host can reach VMs and other machines on that network directly. Host taps are persisted and recreated after an API or VPP restart."}),W?e("div",{class:"banner",role:"alert",children:W}):null,e("div",{class:"table-wrap",children:f.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Interface"}),e("th",{scope:"col",children:"Network"}),e("th",{scope:"col",children:"Host / VPP address"}),e("th",{scope:"col",children:"VPP index"}),e("th",{scope:"col",children:"Created"}),e("th",{scope:"col",children:"Actions"})]})}),e("tbody",{children:f.map(i=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name mono",children:i.name}),e(De,{entityType:"host_tap",entityID:i.id,name:i.name,labels:qn.labels[String(i.id)],onSaved:D=>qn.setEntityLabels(String(i.id),D)})]}),e("td",{children:[e("span",{class:"vm-name",children:i.network_name}),e("span",{class:"vm-description mono",children:["#",i.network_id]})]}),e("td",{class:"mono",children:e("div",{class:"address-stack",children:[e("span",{children:[e("span",{class:"address-side",children:"Host"}),i.ipv4_address||"\u2014"]}),e("span",{children:[e("span",{class:"address-side",children:"VPP"}),i.vpp_addresses&&i.vpp_addresses.length>0?i.vpp_addresses.join(", "):"\u2014"]})]})}),e("td",{class:"mono",children:i.sw_if_index||"\u2014"}),e("td",{children:pt(i.created_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:!y,onClick:()=>j({name:i.name,label:"host tap",hostAddresses:i.ipv4_address?[i.ipv4_address]:[]}),children:"Addresses"}),e("button",{class:"button ghost",type:"button",disabled:!y||!i.sw_if_index,onClick:()=>pe({name:i.name,label:"Host tap"}),children:"Route tables"}),e("button",{class:"button ghost",type:"button",disabled:g===i.name||!y,onClick:()=>{bo(i)},children:g===i.name?"Deleting\u2026":"Delete"})]})})]},i.id))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u21C4"}),e("h3",{children:"No host taps"}),e("p",{children:"Connect a Rookery Network to the Linux host to reach its machines."}),e("button",{class:"button primary",type:"button",disabled:!y||t.length===0,onClick:()=>z(!0),children:"Add host tap"})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"interfaces-heading",hidden:Ie!=="interfaces",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"interfaces-heading",children:"Rookery Virtual Interfaces"}),e("p",{class:"panel-subtitle",children:[Vo,y?e("span",{class:"live-indicator","data-live":kt?"false":"true",children:kt?"counters unavailable":"live counters"}):null]})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",onClick:()=>{en(!0)},children:"Refresh counters"}),e("button",{class:"button ghost",type:"button",disabled:!y,title:y?"":"VPP must be ready to reset interface counters.",onClick:()=>{wo()},children:"Reset counters"}),e("button",{class:"button primary",type:"button",disabled:t.length===0||!y,title:t.length===0?"Create a Rookery Network first":y?"":"Interface creation is blocked until FD.io VPP is ready",onClick:()=>te("/interfaces/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add interface"]})]})]}),v?e("div",{class:"banner",role:"alert",children:v}):null,kt?e("p",{class:"field-hint",children:["Live counters unavailable: ",kt]}):null,e("div",{class:"table-wrap",children:b.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Network"}),e("th",{scope:"col",children:"MAC"}),e("th",{scope:"col",children:"VPP addresses"}),e("th",{scope:"col",children:"Attached to"}),e("th",{scope:"col",children:"TX/RX"}),e("th",{scope:"col",children:"Speed"}),e("th",{scope:"col",children:"ACLs"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:b.map(i=>{let D=i.attached_vm_id?h.find(mt=>mt.id===i.attached_vm_id):null,Y=!!i.attached_vm_id,ce=!!(i.attached_vm_id||i.attached_lightvm_id),be=uo[i.name],He=fo[i.name],qe=!!(D&&(D.status==="running"||D.status==="starting")),Ue=[],Qe=i.ingress_acls?.length?i.ingress_acls:i.ingress_acl_name?[i.ingress_acl_name]:[],ut=i.egress_acls?.length?i.egress_acls:i.egress_acl_name?[i.egress_acl_name]:[];Qe.length>0&&Ue.push(`from: ${Qe.join(" \u2192 ")}`),ut.length>0&&Ue.push(`to: ${ut.join(" \u2192 ")}`);let Je=lo.find(mt=>mt.interface===i.name),nt=ct===i.id;return e(ge,{children:[e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:i.name}),i.socket_path?e("span",{class:"vm-description",title:i.socket_path,children:i.socket_path}):null,e("span",{class:"vm-description",children:["FIB v4: ",i.ipv4_table_name||"default"," \xB7 v6:"," ",i.ipv6_table_name||"default"]}),e(De,{entityType:"interface",entityID:i.id,name:i.name,labels:Wn.labels[String(i.id)],onSaved:mt=>Wn.setEntityLabels(String(i.id),mt)})]}),e("td",{class:"mono",children:i.network_name||"\u2014"}),e("td",{class:"mono",children:i.mac||"\u2014"}),e("td",{class:"mono",children:i.vpp_addresses&&i.vpp_addresses.length>0?i.vpp_addresses.join(", "):"\u2014"}),e("td",{class:ce?"mono":"mono muted",children:D?D.name:i.attached_vm_name||(i.attached_lightvm_name?`${i.attached_lightvm_name} (LightVM)`:"unattached")}),e("td",{class:be?"mono traffic-cell":"mono muted traffic-cell",children:be?e(ge,{children:[e("span",{class:"traffic-line",children:["\u2193 ",Ut(be.rx_bytes??0)]}),e("span",{class:"traffic-line",children:["\u2191 ",Ut(be.tx_bytes??0)]})]}):"\u2014"}),e("td",{class:He?"mono traffic-cell":"mono muted traffic-cell",children:He?e(ge,{children:[e("span",{class:"traffic-line",children:["\u2193 ",Ut(He.rx),"/s"]}),e("span",{class:"traffic-line",children:["\u2191 ",Ut(He.tx),"/s"]})]}):"\u2014"}),e("td",{class:Ue.length?"mono":"mono muted",children:Ue.length?e("div",{class:"interface-acl-summary",children:[e("span",{class:"acl-assignment",children:Ue.join(" \xB7 ")}),Je?e("span",{class:"acl-inline-counter",children:["from ",Ye(Je.ingress?.matched)," \xB7 to"," ",Ye(Je.egress?.matched)]}):Rn?Jt?e("span",{class:"acl-inline-counter muted",children:"counters unavailable"}):null:e("span",{class:"acl-inline-counter muted",children:"loading counters\u2026"}),e("button",{class:"row-action",type:"button",onClick:()=>Ve(nt?null:i.id),children:nt?"Hide details":"ACL details"})]}):"none"}),e("td",{class:"updated hide-sm",children:pt(i.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",disabled:!y,title:y?"":"VPP must be ready to set interface addresses.",onClick:()=>j({name:i.name,label:"vhost-user"}),children:"Addresses"}),e("button",{class:"row-action",type:"button",disabled:!!i.attached_lightvm_id,title:i.attached_lightvm_id?"Remove the interface from its LightVM first.":"",onClick:()=>te(`/interfaces/${i.id}/attach`),children:"Attach"}),Y?e("button",{class:"row-action",type:"button",disabled:qe,title:qe?"Stop the VM before detaching its interface.":"",onClick:()=>{To(i)},children:"Detach"}):null,!Y&&i.attached_lightvm_id?e("button",{class:"row-action",type:"button",onClick:()=>te(`/lightvm/instances/${i.attached_lightvm_id}/edit`),children:"Edit LightVM"}):null,e("button",{class:"row-action",type:"button",disabled:!y,title:y?"":"VPP must be ready to apply interface ACLs.",onClick:()=>te(`/interfaces/${i.id}/acls`),children:"ACLs"}),e("button",{class:"row-action",type:"button",disabled:!y,title:y?"":"VPP must be ready to change interface FIB tables.",onClick:()=>te(`/interfaces/${i.id}/route-tables`),children:"Route tables"}),e("button",{class:"row-action danger",type:"button",disabled:ce||!y,title:ce?"Detach the interface from its VM or LightVM first.":y?"":"VPP must be ready to delete a vhost-user interface.",onClick:()=>{So(i)},children:"Delete"})]})})]}),nt&&Ue.length>0?e("tr",{class:"acl-details-row",children:e("td",{colSpan:10,children:e("div",{class:"interface-acl-details",children:[e("div",{class:"interface-acl-details-head",children:[e("strong",{children:["ACL details for ",i.name]}),e("button",{class:"button ghost",type:"button",onClick:()=>te(`/interfaces/${i.id}/acls`),children:"Edit ACLs"})]}),Je?e(ge,{children:[e("div",{class:"acl-details-summary",children:[e("span",{children:[e("b",{children:"From interface"})," ",Yn(Je.ingress)]}),e("span",{children:[e("b",{children:"To interface"})," ",Yn(Je.egress)]})]}),e("div",{class:"acl-rule-counter-list",children:[Qn("From interface rules",Je.ingress),Qn("To interface rules",Je.egress)]})]}):Jt?e("p",{class:"field-hint",children:["ACL counters unavailable: ",Jt]}):Rn?e("p",{class:"field-hint",children:"No ACL counter entry for this interface yet. Generate traffic or refresh the interface list."}):e("p",{class:"field-hint",children:"Loading ACL counters\u2026"}),e("p",{class:"field-hint",children:"Counters show explicit ACL rule matches; implicit-deny packets are not included."})]})})}):null]},i.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u21C4"}),e("h3",{children:"No virtual interfaces yet"}),e("p",{children:"Add a VPP vhost-user interface to a Rookery Network, then attach it to a VM."})]})})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"acls-heading",hidden:Ie!=="acl",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"acls-heading",children:"Network ACLs"}),e("p",{class:"panel-subtitle",children:Mo})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",disabled:L,onClick:()=>{rt()},children:L?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",disabled:!y,title:y?"":"ACL changes are blocked until FD.io VPP is ready.",onClick:()=>te("/acls/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add ACL"]})]})]}),k?e("div",{class:"banner",role:"alert",children:k}):null,e("div",{class:"table-wrap",children:d.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Title"}),e("th",{scope:"col",children:"Rules"}),e("th",{scope:"col",children:"Assigned to"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:d.map(i=>{let D=Bt(i,b);return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:i.name}),i.description?e("span",{class:"vm-description",title:i.description,children:i.description}):null,e(De,{entityType:"acl",entityID:i.id,name:i.name,labels:zn.labels[String(i.id)],onSaved:Y=>zn.setEntityLabels(String(i.id),Y)})]}),e("td",{children:i.title||"\u2014"}),e("td",{class:"mono",children:[i.rules?.length||0," rule",i.rules?.length===1?"":"s"]}),e("td",{class:D.length?"mono":"mono muted",children:D.length?D.map(Y=>Y.name).join(", "):"none"}),e("td",{class:"updated hide-sm",children:pt(i.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",disabled:!y,title:y?"":"VPP must be ready to edit ACLs.",onClick:()=>te(`/acls/${i.id}/edit`),children:"Edit / Preview"}),e("button",{class:"row-action danger",type:"button",disabled:D.length>0,title:D.length>0?"Remove this ACL from its interfaces first.":"",onClick:()=>{Po(i)},children:"Delete"})]})})]},i.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25A4"}),e("h3",{children:"No ACLs yet"}),e("p",{children:"Define ordered ingress/egress rules, then assign an ACL to a virtual interface."}),e("button",{class:"button primary",type:"button",disabled:!y,onClick:()=>te("/acls/new"),children:"Add an ACL"})]})})]}),e("section",{class:"panel panel-spaced","aria-label":"Route tables",hidden:Ie!=="routes",children:xe?e(vs,{table:xe.table,routeID:xe.routeID,interfaces:b,bviInterfaces:t.filter(i=>!!i.bvi_host_name).map(i=>i.bvi_host_name||"").filter(Boolean),hostInterfaces:s,hostTaps:f,tables:X,onBack:()=>{Oe(null),te(`/route-tables/${xe.table.id}?ipv6=${xe.table.ipv6}`,!0)},onSaved:()=>{let i=xe.table;Oe(null),te(`/route-tables/${i.id}?ipv6=${i.ipv6}`,!0),ae(xe.routeID?`Saved route ${i.name}`:`Added route to ${i.name}`),dt(!1)}},`route-form-${xe.table.id}-${xe.routeID??"new"}`):we?e(_s,{table:we,onBack:()=>{Te(null),te("/networks/routes",!0)},onChanged:()=>{dt(!1)}},`route-page-${we.ipv6?"v6":"v4"}-${we.id}`):Ee?e(ys,{tables:X,onCancel:()=>{U(!1),te("/networks/routes",!0)},onSaved:i=>{U(!1),te("/networks/routes",!0),ae(`Created route table ${i.name}`),dt()}}):e(ge,{children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"routes-heading",children:"Route tables"}),e("p",{class:"panel-subtitle",children:"IP FIB / VRF definitions attachable to interfaces. Table 0 is the default VRF."})]}),e("button",{class:"button primary",type:"button",disabled:!y,onClick:()=>te("/route-tables/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add route table"]})]}),H?e("div",{class:"banner",role:"alert",children:H}):null,e("div",{class:"table-wrap",children:e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Family"}),e("th",{scope:"col",children:"Table ID"}),e("th",{scope:"col",children:"Interfaces"}),e("th",{scope:"col",children:"Routes"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:X.map(i=>e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:i.name}),i.is_default?e("span",{class:"vm-description",children:"default VRF"}):null,i.network_id?e("span",{class:"vm-description",children:["network VRF \xB7 network ",i.network_id]}):null,i.is_default?null:e(De,{entityType:"route_table",entityID:i.id,name:i.name,labels:Gn.labels[String(i.id)],onSaved:D=>Gn.setEntityLabels(String(i.id),D)})]}),e("td",{class:"mono",children:i.ipv6?"IPv6":"IPv4"}),e("td",{class:"mono",children:i.table_id}),e("td",{class:i.interface_count?"mono":"mono muted",children:i.interface_count||0}),e("td",{class:i.route_count?"mono":"mono muted",children:i.route_count||0}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:"row-action",type:"button",onClick:()=>te(`/route-tables/${i.id}?ipv6=${i.ipv6}`),children:"Routes"}),i.is_default?e("span",{class:"muted",children:"built-in"}):i.managed?e("span",{class:"muted",children:"network-managed"}):e("button",{class:"row-action danger",type:"button",disabled:!y,onClick:()=>{Eo(i)},children:"Delete"})]})})]},`${i.ipv6?"v6":"v4"}-${i.id}`))})]})})]})}),e("section",{class:"panel panel-spaced","aria-labelledby":"troubleshoot-heading",hidden:Ie!=="troubleshoot",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"troubleshoot-heading",children:"Troubleshoot"}),e("p",{class:"panel-subtitle",children:"VPP packet processing graph"})]}),e("button",{class:"button ghost",type:"button",disabled:Kt,onClick:()=>{Xn(!0)},children:Kt?"Refreshing\u2026":"Refresh graph"})]}),e("p",{class:"field-hint",children:"Active VPP nodes since the last stats clear. Node size represents cycles per packet; color represents packets per call."}),it?e("div",{class:"banner",role:"alert",children:it}):null,Kt&&!Ae?e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"Loading VPP graph\u2026"})]}):Ae?e(Ls,{graph:Ae}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"No graph loaded"}),e("p",{children:"Use Refresh graph to inspect the active VPP packet-processing nodes."})]}),Ae?.generated_at?e("p",{class:"field-hint",children:["Sampled ",pt(Ae.generated_at)]}):null]}),e("section",{class:"panel panel-spaced","aria-labelledby":"trace-heading",hidden:Ie!=="troubleshoot",children:[e("div",{class:"panel-head",children:e("div",{children:[e("h2",{id:"trace-heading",children:"Packet trace"}),e("p",{class:"panel-subtitle",children:"Trace packets through a selected VPP graph node"})]})}),e(gs,{active:Ie==="troubleshoot"})]}),e("section",{class:"panel panel-spaced","aria-labelledby":"capture-heading",hidden:Ie!=="troubleshoot",children:[e("div",{class:"panel-head",children:e("div",{children:[e("h2",{id:"capture-heading",children:"Packet capture"}),e("p",{class:"panel-subtitle",children:"Capture VPP traffic and download pcap files"})]})}),e(hs,{})]}),ie?e(fs,{networks:t,onClose:()=>x(!1),onSaved:i=>{x(!1),l(D=>[...D,i].sort((Y,ce)=>Y.id-ce.id)),B(""),ae(`Created pipe ${i.network_a_name} \u2194 ${i.network_b_name}`)}}):null,le?e(ps,{network:fe,onClose:()=>{se(!1),ee(null),te("/networks",!0)},onSaved:i=>{let D=!!fe;se(!1),ee(null),te("/networks",!0);let Y=D?t.map(ce=>ce.id===i.id?i:ce):[...t,i];Be({networks:Y.sort((ce,be)=>ce.name.localeCompare(be.name,void 0,{sensitivity:"base"}))}),p(""),ae(D?`Updated network ${i.name}`:`Created network ${i.name}`)}}):null,de?e(ds,{networks:t,vms:h,onClose:()=>{me(!1),te("/networks/interfaces",!0)},onSaved:i=>{me(!1),te("/networks/interfaces",!0),Be({interfaces:[...b,i].sort((D,Y)=>D.name.localeCompare(Y.name,void 0,{sensitivity:"base"}))}),M(""),ae(`Created interface ${i.name}`),Qt()}}):null,$?e(ws,{name:$.name,label:$.label,hostAddresses:$.hostAddresses,onClose:()=>j(null)},`addresses-${$.name}`):null,J?e(ks,{name:J.name,label:J.label,tables:X,onClose:()=>pe(null),onSaved:i=>{pe(null),ae(`Saved route tables for ${i.name}`)}},`route-tables-${J.name}`):null,u?e(is,{iface:u,vms:h,onClose:()=>{_(null),te("/networks/interfaces",!0)},onSaved:i=>{_(null),te("/networks/interfaces",!0),Be({interfaces:b.map(D=>D.id===i.id?i:D)}),M(""),ae(i.attached_vm_id?`Attached ${i.name} to ${i.attached_vm_name||"VM"}`:`Detached ${i.name}`),Qt()}},`attach-${u.id}`):null,Le?e(os,{acl:Le.acl,vppReady:y,onClose:()=>{q(null),te("/networks/acl",!0)},onSaved:i=>{q(null),te("/networks/acl",!0),Be({acls:Le.acl?d.map(D=>D.id===i.id?i:D):[...d,i].sort((D,Y)=>D.name.localeCompare(Y.name,void 0,{sensitivity:"base"}))}),O(""),ae(`${Le.acl?"Saved":"Added"} ACL ${i.name}`),jn(!1)}},Le.acl?`edit-${Le.acl.id}`:"new"):null,he?e(cs,{iface:he,acls:d,vppReady:y,onClose:()=>{ve(null),te("/networks/interfaces",!0)},onSaved:i=>{ve(null),te("/networks/interfaces",!0),Be({interfaces:b.map(D=>D.id===i.id?i:D)}),M(""),en(!0),ae(`Applied ACLs to ${i.name}`)}},`acl-${he.id}`):null,et?e(us,{iface:et,tables:X,onClose:()=>{Ke(null),te("/networks/interfaces",!0)},onSaved:i=>{Ke(null),te("/networks/interfaces",!0),Be({interfaces:b.map(D=>D.id===i.id?i:D)}),M(""),ae(`Updated route tables for ${i.name}`),dt(!1)}},`route-tables-${et.id}`):null,E?e(rs,{networks:t,onClose:()=>z(!1),onSaved:i=>{z(!1),r(D=>[...D,i].sort((Y,ce)=>Y.name.localeCompare(ce.name,void 0,{sensitivity:"base"}))),Q(""),ae(`Created host tap ${i.name}`)}}):null]})}function Ds(t){t&&Fe(e(Ns,{}),t)}function Ms(t){if(!t)return[];let n=new Set;for(let a of t.split(",")){let l=a.trim();if(!l)continue;let[s,o,f]=l.split("-"),r=Number(s);if(!Number.isInteger(r)||r<0)continue;let b=f?Number(o):r;if(!(!Number.isInteger(b)||ba-l)}function Hi(t){let n=[...new Set(t)].filter(l=>Number.isInteger(l)&&l>=0).sort((l,s)=>l-s),a=[];for(let l=0;l0?e("div",{class:"cpu-grid",role:"group","aria-label":"Allowed CPU cores",children:n.map(f=>e("button",{type:"button",class:o.has(f)?"cpu-chip selected":"cpu-chip","aria-pressed":o.has(f),disabled:s,onClick:()=>a(Oi(t,f)),children:f},f))}):null,e("input",{type:"text",value:t,disabled:s,placeholder:"e.g. 0-3,7 (empty means all CPUs)",onInput:f=>a(f.currentTarget.value)}),l?e("span",{class:"field-hint",children:l}):null]})}var $s=1024*1024;function xs(t){return t?String(Math.round(t/$s*10)/10):""}function Vs(t){let n=Number(t);return!Number.isFinite(n)||n<=0?0:Math.round(n*$s)}function Sn(t){return t===void 0||t===0?"":String(t)}function Tn(t){let n=Number(t);return Number.isFinite(n)&&n>0?Math.floor(n):0}function En({limits:t,devices:n,onChange:a,disabled:l}){let s=new Map(n.map(d=>[d.path,d])),o=new Set(t.map(d=>d.device)),f=n.filter(d=>!o.has(d.path)),r=(d,C)=>{a(t.map((h,A)=>A===d?{...h,...C}:h))},b=()=>{let d=f[0];d&&a([...t,{device:d.path}])},w=d=>{a(t.filter((C,h)=>h!==d))};return e("div",{class:"cgroup-io-editor",children:[e("div",{class:"table-wrap",children:e("table",{class:"cgroup-io-table",children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Block device"}),e("th",{scope:"col",children:"Read MB/s"}),e("th",{scope:"col",children:"Write MB/s"}),e("th",{scope:"col",children:"Read IOPS"}),e("th",{scope:"col",children:"Write IOPS"}),e("th",{scope:"col",children:"Weight"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.length===0?e("tr",{children:e("td",{colspan:7,class:"muted",children:"No per-device I/O limits. Add a block device to constrain it."})}):t.map((d,C)=>e("tr",{children:[e("td",{children:e("select",{value:d.device,disabled:l,onChange:h=>r(C,{device:h.currentTarget.value}),children:[s.has(d.device)?null:e("option",{value:d.device,children:d.device}),n.map(h=>e("option",{value:h.path,disabled:o.has(h.path)&&h.path!==d.device,children:[h.path,h.model?` \xB7 ${h.model}`:""]},h.path))]})}),e("td",{children:e("input",{type:"number",min:0,step:"any",disabled:l,value:xs(d.read_bps),placeholder:"unlimited",onInput:h=>r(C,{read_bps:Vs(h.currentTarget.value)})})}),e("td",{children:e("input",{type:"number",min:0,step:"any",disabled:l,value:xs(d.write_bps),placeholder:"unlimited",onInput:h=>r(C,{write_bps:Vs(h.currentTarget.value)})})}),e("td",{children:e("input",{type:"number",min:0,step:1,disabled:l,value:Sn(d.read_iops),placeholder:"unlimited",onInput:h=>r(C,{read_iops:Tn(h.currentTarget.value)})})}),e("td",{children:e("input",{type:"number",min:0,step:1,disabled:l,value:Sn(d.write_iops),placeholder:"unlimited",onInput:h=>r(C,{write_iops:Tn(h.currentTarget.value)})})}),e("td",{children:e("input",{type:"number",min:0,max:1e4,step:1,disabled:l,value:Sn(d.weight),placeholder:"inherit",onInput:h=>r(C,{weight:Tn(h.currentTarget.value)})})}),e("td",{children:e("button",{class:"row-action danger",type:"button",disabled:l,onClick:()=>w(C),children:"Remove"})})]},d.device))})]})}),e("div",{class:"actions",children:e("button",{class:"button ghost",type:"button",disabled:l||f.length===0,onClick:b,children:"Add block device"})})]})}var Rs=1024*1024;function Ln(t){return t?String(Math.round(t/Rs)):""}function Fs(t){let n=Number(t);return!Number.isFinite(n)||n<=0?0:Math.round(n*Rs)}function Bs(t){return t?String(t):""}function Hs(t){let n=Number(t);return Number.isFinite(n)&&n>0?Math.floor(n):0}function _t(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(1)} MB`:n>=1024?`${(n/1024).toFixed(1)} KB`:`${n} B`}function qt(t){return t===void 0?"\u2014":`${_t(t)}/s`}function Ui(t){let n=_t(t.memory_current_bytes);return t.memory_max_bytes>0?`${n} / ${_t(t.memory_max_bytes)}`:`${n} / unlimited`}function qi(t,n){return n<=0?0:Math.max(0,Math.min(100,t/n*100))}function Os(){let[t,n]=c(null),[a,l]=c([]),[s,o]=c(!1),[f,r]=c(""),[b,w]=c(""),[d,C]=c(""),[h,A]=c(""),[I,m]=c([]),[y,P]=c([]),[L,S]=c(!0),[N,p]=c(!1),[T,B]=c(""),[K,Z]=c(null),[ie,x]=c({}),[R,F]=c(""),[W,Q]=c(!1),g=ne(null),oe=ze(),E=q=>{n(q),o(!!q.enabled),r(q.cpus||""),w(q.mems||""),C(Ln(q.memory_max_bytes)),A(Bs(q.cpu_weight)),m(Array.isArray(q.io_limits)?q.io_limits:[]),P(Array.isArray(q.classes)?q.classes:[])},z=async()=>{S(!0);try{let[q,he]=await Promise.all([V("/api/cgroup-settings"),V("/api/host/block-devices").catch(()=>[])]);E(q),l(Array.isArray(he)?he:[]),B("")}catch(q){B(q instanceof Error?q.message:String(q))}finally{S(!1)}},re=async()=>{Q(!0);try{let q=await V("/api/cgroup-usage"),he=q.sampled_at?new Date(q.sampled_at).getTime():Date.now(),ve=Array.isArray(q.nodes)?q.nodes:[],X=g.current,$e={};if(X&&Number.isFinite(he)&&he>X.at){let H=(he-X.at)/1e3;for(let _e of ve){let Ee=_e.path||"",U=X.nodes[Ee];U&&($e[Ee]={cpuCores:Math.max(0,(_e.cpu_usage_usec-U.cpu)/1e6/H),read:Math.max(0,(_e.io_read_bytes-U.read)/H),write:Math.max(0,(_e.io_write_bytes-U.write)/H)})}}g.current={at:Number.isFinite(he)?he:Date.now(),nodes:Object.fromEntries(ve.map(H=>[H.path||"",{cpu:H.cpu_usage_usec,read:H.io_read_bytes,write:H.io_write_bytes}]))},x($e),Z(q),F("")}catch(q){F(q instanceof Error?q.message:String(q))}finally{Q(!1)}};G(()=>{z()},[]);let ue=oe.segments[0]==="settings";G(()=>{if(!ue)return;re();let q=window.setInterval(()=>{document.hidden||re()},2e3);return()=>window.clearInterval(q)},[ue]);let v=async q=>{if(q.preventDefault(),!N){p(!0),B("");try{let he=await V("/api/cgroup-settings",{method:"PUT",body:JSON.stringify({enabled:s,cpus:f.trim(),mems:b.trim(),memory_max_bytes:Fs(d),cpu_weight:Hs(h),io_limits:I.map(ve=>({device:ve.device,read_bps:ve.read_bps||0,write_bps:ve.write_bps||0,read_iops:ve.read_iops||0,write_iops:ve.write_iops||0,weight:ve.weight||0})),classes:y.map(ve=>({name:ve.name.trim(),cpus:(ve.cpus||"").trim(),memory_max_bytes:ve.memory_max_bytes||0,cpu_weight:ve.cpu_weight||0,io_limits:(ve.io_limits||[]).map(X=>({device:X.device,read_bps:X.read_bps||0,write_bps:X.write_bps||0,read_iops:X.read_iops||0,write_iops:X.write_iops||0,weight:X.weight||0}))}))})});E(he),re(),ae(s?"Saved cgroup resource settings":"Cgroup limits disabled")}catch(he){B(he instanceof Error?he.message:String(he))}finally{p(!1)}}},M=Array.isArray(t?.online_cpus)?t.online_cpus:[],k=!!t?.available,O=!!t?.enabled,le=t?.reason||"",se=O?t?.configured?`Applied at ${t?.root||"/sys/fs/cgroup/rookery-cgroup"}`:"Enabled but not applied yet":"Disabled",fe=Array.isArray(K?.nodes)?K.nodes:[],ee=fe.find(q=>!q.path)||null,de=fe.filter(q=>{let he=q.path||"";return he?!fe.some(ve=>{let X=ve.path||"";return X!==""&&X.startsWith(`${he}/`)}):!1}),me=de.length>0?de:ee?[ee]:[],u=me.reduce((q,he)=>q+(ie[he.path||""]?.cpuCores||0),0),_=me.reduce((q,he)=>q+(ie[he.path||""]?.read||0),0),$=me.reduce((q,he)=>q+(ie[he.path||""]?.write||0),0),j=K?.online_cpus||M.length||0,J=j>0?u/j*100:0,pe=ee?qi(ee.memory_current_bytes,ee.memory_max_bytes):0,Le=K?.sampled_at?`sampled ${new Date(K.sampled_at).toLocaleTimeString()}`:"waiting for sample";return e("section",{class:"panel panel-spaced","aria-labelledby":"cgroup-settings-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"cgroup-settings-heading",children:"Cgroup v2 resources"}),e("p",{class:"panel-subtitle",children:"Host-wide ceilings inherited by every QEMU process"})]}),e("button",{class:"button ghost",type:"button",disabled:L||N,onClick:()=>{z(),re()},children:L?"Refreshing\u2026":"Refresh"})]}),e("p",{class:"field-hint",children:["Rookery runs QEMU below ",t?.root||"/sys/fs/cgroup/rookery-cgroup",". VMs without a cgroup class run in the shared child cgroup; each class is a cgroup node below the",e("code",{children:"rookery-cgroup"})," root, for example ",e("code",{children:"rookery-cgroup/prod/web"}),"."]}),e("div",{class:"cgroup-status",children:[e("span",{class:"status-badge","data-status":k?O?t?.configured?"running":"paused":"disabled":"error",children:k?O?t?.configured?"applied":"not applied":"disabled":"unavailable"}),e("span",{class:"field-hint",children:[se,t?.mount?` \xB7 mount ${t.mount}`:"",M.length>0?` \xB7 ${M.length} online CPUs`:"",y.length>0?` \xB7 ${y.length} classes`:"",t?.cpu_model?` \xB7 ${t.cpu_model}`:""]})]}),le?e("div",{class:"banner",role:"alert",children:le}):null,e("section",{class:"cgroup-telemetry","aria-labelledby":"cgroup-usage-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h3",{id:"cgroup-usage-heading",children:"Live cgroup usage"}),e("p",{class:"panel-subtitle",children:["Memory, CPU, task, and I/O telemetry from the running hierarchy \xB7 ",Le]})]}),e("button",{class:"button ghost",type:"button",disabled:W,onClick:()=>{re()},children:W?"Sampling\u2026":"Refresh usage"})]}),R?e("div",{class:"banner",role:"alert",children:R}):K?K.available?K.configured?fe.length===0?e("p",{class:"field-hint",children:K.reason||"No cgroup nodes are available yet."}):e(ge,{children:[e("div",{class:"cgroup-metrics",children:[e("div",{class:"cgroup-metric",children:[e("span",{class:"field-label",children:"Memory"}),e("strong",{children:ee?_t(ee.memory_current_bytes):"\u2014"}),e("span",{class:"field-hint",children:ee&&ee.memory_max_bytes>0?`of ${_t(ee.memory_max_bytes)} \xB7 ${pe.toFixed(1)}%`:"unlimited"}),ee&&ee.memory_max_bytes>0?e("div",{class:"usage-bar",children:e("span",{style:{width:`${pe}%`}})}):null]}),e("div",{class:"cgroup-metric",children:[e("span",{class:"field-label",children:"CPU"}),e("strong",{children:[u.toFixed(2)," cores"]}),e("span",{class:"field-hint",children:j>0?`${J.toFixed(1)}% of ${j} online CPUs`:"online CPU count unavailable"})]}),e("div",{class:"cgroup-metric",children:[e("span",{class:"field-label",children:"Tasks"}),e("strong",{children:ee?.pids_current??"\u2014"}),e("span",{class:"field-hint",children:ee&&ee.pids_max>0?`of ${ee.pids_max}`:"no task limit"})]}),e("div",{class:"cgroup-metric",children:[e("span",{class:"field-label",children:"I/O"}),e("strong",{children:qt(_)}),e("span",{class:"field-hint",children:["read \xB7 ",qt($)," write"]})]})]}),e("div",{class:"table-wrap",children:e("table",{class:"cgroup-usage-table",children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Scope"}),e("th",{scope:"col",children:"Memory"}),e("th",{scope:"col",children:"CPU"}),e("th",{scope:"col",children:"Tasks"}),e("th",{scope:"col",children:"I/O"})]})}),e("tbody",{children:fe.map(q=>{let he=q.path||"",ve=ie[he];return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name mono",children:q.name||he||"rookery-cgroup"}),he?e("span",{class:"vm-description mono",children:he}):null]}),e("td",{class:"mono",children:Ui(q)}),e("td",{class:"mono",children:ve?`${ve.cpuCores.toFixed(2)} cores`:"\u2014"}),e("td",{class:"mono",children:[q.pids_current,q.pids_max>0?` / ${q.pids_max}`:""]}),e("td",{class:"mono",children:ve?`\u2193 ${qt(ve.read)} \xB7 \u2191 ${qt(ve.write)}`:"\u2014"})]},he||"root")})})]})})]}):e("p",{class:"field-hint",children:K.reason||"Enable cgroup limits to start collecting resource usage."}):e("p",{class:"field-hint",children:"cgroup v2 is not available on this host."}):e("p",{class:"field-hint",children:"Waiting for the first cgroup sample\u2026"})]}),e("form",{class:"form-grid",onSubmit:q=>{v(q)},children:[e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:s,onChange:q=>o(q.currentTarget.checked)}),e("span",{children:"Enable cgroup v2 limits for QEMU"})]}),e("span",{class:"field-hint",children:"Saving applies the new host defaults to every running VM immediately: Rookery re-places each live QEMU process in its shared or class cgroup. New and restarted VMs are always placed under this hierarchy. When disabled, running QEMU processes are moved out immediately and the Rookery cgroups are removed; all VMs then run unconstrained, and class limits take effect again when host limits are enabled. Shrinking the host CPU set clamps running VMs to the remaining CPUs."})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"CPU cores"}),e(Pn,{value:f,options:M,disabled:!s,onChange:r,hint:"Click cores to pin QEMU to them. The text field accepts ranges. Empty means all online CPUs."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Total memory limit (MB)"}),e("input",{type:"number",min:0,step:128,disabled:!s,value:d,placeholder:"unlimited",onInput:q=>C(q.currentTarget.value)}),e("span",{class:"field-hint",children:"memory.max for the whole Rookery cgroup subtree. Empty means unlimited."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"CPU weight"}),e("input",{type:"number",min:0,max:1e4,step:1,disabled:!s,value:h,placeholder:"default (100)",onInput:q=>A(q.currentTarget.value)}),e("span",{class:"field-hint",children:"cpu.weight (1\u201310000). Empty uses the kernel default of 100."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Memory nodes ",e("em",{children:"optional"})]}),e("input",{type:"text",disabled:!s,value:b,placeholder:"e.g. 0-1 (empty means all)",onInput:q=>w(q.currentTarget.value)}),e("span",{class:"field-hint",children:"cpuset.mems NUMA node list."})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Per-device I/O limits"}),e(En,{limits:I,devices:a,disabled:!s,onChange:m}),a.length===0?e("span",{class:"field-hint",children:["No whole block devices were reported by the host. Check access to ",e("code",{children:"/sys/class/block"}),"."]}):null,e("span",{class:"field-hint",children:"io.max limits read/write bandwidth (MiB/s) and IOPS. io.weight sets the proportional weight for that device (1\u201310000; requires the BFQ scheduler)."})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Classes"}),e("span",{class:"field-hint",children:["Define a tree of reusable profiles below ",e("code",{children:"rookery-cgroup"}),". Class paths are slash-separated and may be up to 3 levels deep, for example ",e("code",{children:"prod"}),","," ",e("code",{children:"prod/web"}),", or ",e("code",{children:"prod/web/frontend"}),". Define the parent class first. VMs can only be assigned to leaf classes; parent classes group their children and inherit limits. Assign a class to a VM instead of entering limits per VM. Class limits are shared by every VM assigned to that class; allow enough memory for all of them."]}),e(ji,{classes:y,devices:a,onlineCPUs:M,disabled:L||N,onChange:P})]}),T?e("p",{class:"form-error field-wide",role:"alert",children:T}):null,e("div",{class:"field-wide actions",children:e("button",{class:"button primary",type:"submit",disabled:N||L,children:N?"Saving\u2026":"Save cgroup settings"})})]})]})}function Wi(t,n){let a=1;for(;;){let l=n?`${n}/class-${a}`:`class-${a}`;if(!t.some(s=>s.name===l))return l;a++}}function Us(t){let n=t.lastIndexOf("/");return n>0?t.slice(0,n):""}function zi(t){let n=t.lastIndexOf("/");return n>=0?t.slice(n+1):t}function Gi(t,n,a){let l=a.trim();if(!l||l.includes("/"))return null;let s=Us(n),o=s?`${s}/${l}`:l;return o===n?t:t.some(f=>f.name===o)?null:t.map(f=>f.name===n?{...f,name:o}:f.name.startsWith(n+"/")?{...f,name:o+f.name.slice(n.length)}:f)}function Ki(t){let n=[];return t.cpus&&n.push(`CPU ${t.cpus}`),t.memory_max_bytes&&n.push(`${Ln(t.memory_max_bytes)} MB`),t.cpu_weight&&n.push(`weight ${t.cpu_weight}`),t.io_limits?.length&&n.push(`${t.io_limits.length} I/O`),n.length?n.join(" \xB7 "):"inherits host limits"}function ji({classes:t,devices:n,onlineCPUs:a,disabled:l,onChange:s}){let o=(h,A)=>{s(t.map(I=>I.name===h?{...I,...A}:I))},f=h=>{s(t.filter(A=>A.name!==h&&!A.name.startsWith(h+"/")))},r=h=>{s([...t,{name:Wi(t,h)}])},b=new Map(t.map(h=>[h.name,h])),w=new Map,d=[];for(let h of t){let A=Us(h.name);if(A&&b.has(A)){let I=w.get(A)||[];I.push(h),w.set(A,I)}else d.push(h)}for(let h of w.values())h.sort((A,I)=>A.name.localeCompare(I.name));d.sort((h,A)=>h.name.localeCompare(A.name));let C=h=>{let A=h.name.split("/").length,I=w.get(h.name)||[],m=zi(h.name);return e("div",{class:"cgroup-tree-node",children:[e("div",{class:"cgroup-tree-node-head",children:[e("div",{class:"cgroup-tree-title",children:[e("input",{class:"cgroup-tree-name",type:"text",defaultValue:m,disabled:l,"aria-label":`Class name for ${h.name}`,onBlur:y=>{let P=Gi(t,h.name,y.currentTarget.value);if(!P){y.currentTarget.value=m;return}s(P)}}),e("span",{class:"cgroup-tree-path",children:h.name}),e("span",{class:"field-hint",children:Ki(h)})]}),e("div",{class:"actions",children:[A<3?e("button",{class:"row-action",type:"button",disabled:l||t.length>=64,onClick:()=>r(h.name),children:"Add child"}):null,e("button",{class:"row-action danger",type:"button",disabled:l||I.length>0,title:I.length>0?"Remove child classes first.":"",onClick:()=>f(h.name),children:"Remove"})]})]}),e("details",{class:"cgroup-tree-limits",children:[e("summary",{children:"Limits"}),e("div",{class:"form-grid cgroup-class-grid",children:[e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"CPU cores"}),e(Pn,{value:h.cpus||"",options:a,disabled:l,onChange:y=>o(h.name,{cpus:y}),hint:"Empty inherits the host/parent class CPU set. A non-empty set must be a subset of it."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Memory limit (MB)"}),e("input",{type:"number",min:0,step:128,disabled:l,value:Ln(h.memory_max_bytes),placeholder:"inherit",onInput:y=>o(h.name,{memory_max_bytes:Fs(y.currentTarget.value)})})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"CPU weight"}),e("input",{type:"number",min:0,max:1e4,step:1,disabled:l,value:Bs(h.cpu_weight),placeholder:"inherit",onInput:y=>o(h.name,{cpu_weight:Hs(y.currentTarget.value)})})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Per-device I/O limits"}),e(En,{limits:h.io_limits||[],devices:n,disabled:l,onChange:y=>o(h.name,{io_limits:y})})]})]})]}),I.length>0?e("div",{class:"cgroup-tree-children",children:I.map(C)}):null]},h.name)};return e("div",{class:"cgroup-tree",children:[e("div",{class:"cgroup-tree-root",children:[e("div",{children:[e("span",{class:"cgroup-tree-root-name",children:"rookery-cgroup"}),e("span",{class:"field-hint",children:"Root limits are the host settings above; classes nest up to 3 levels below."})]}),e("button",{class:"button ghost",type:"button",disabled:l||t.length>=64,onClick:()=>r(),children:"Add top-level class"})]}),d.length>0?d.map(C):e("p",{class:"field-hint",children:"No classes defined. VMs without a class run in the shared cgroup."})]})}function Ji(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function Xi(t){switch(t){case"success":return"running";case"failed":return"error";case"running":return"starting";case"pending":return"paused";default:return"disabled"}}function qs({kind:t,title:n,endpoint:a,description:l,placeholder:s,spaced:o}){let[f,r]=c(null),[b,w]=c(!1),[d,C]=c(""),[h,A]=c(!0),[I,m]=c(!1),[y,P]=c(""),L=t==="rookery"?"Rookery":"VPP",S=`${t}-startup-script-heading`,N=async()=>{A(!0);try{let Z=await V(a);r(Z),w(!!Z.enabled),C(Z.script||""),P("")}catch(Z){P(Z instanceof Error?Z.message:String(Z))}finally{A(!1)}};G(()=>{N()},[]);let p=async Z=>{if(Z.preventDefault(),!I){if(b&&!d.trim()){P(`Enter a ${L} script before enabling it.`);return}m(!0),P("");try{let ie=await V(a,{method:"PUT",body:JSON.stringify({enabled:b,script:d})});r(ie),w(!!ie.enabled),C(ie.script||""),ae(ie.enabled?`Saved ${L} startup script${t==="rookery"?"; it will run on the next API start":"; it will run on the next VPP connection"}`:`Saved ${L} startup script settings`)}catch(ie){P(ie instanceof Error?ie.message:String(ie))}finally{m(!1)}}},T=f?.status||"disabled",B=f?.output||"",K=T==="success"?0:f?.exit_code??"\u2014";return e("section",{class:o?"panel panel-spaced":"panel","aria-labelledby":S,children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:S,children:n}),e("p",{class:"panel-subtitle",children:t==="rookery"?"Rookery server lifecycle hook":"Applied whenever Rookery connects to FD.io VPP"})]}),e("button",{class:"button ghost",type:"button",disabled:h||I,onClick:()=>{N()},children:h?"Refreshing\u2026":"Refresh"})]}),e("p",{class:"field-hint",children:l}),e("div",{class:"startup-script-status",children:[e("span",{class:"status-badge","data-status":Xi(T),children:T}),T==="success"||T==="failed"?e("span",{class:"field-hint",children:["exit code ",K,f?.ran_at?` \xB7 ${Ji(f.ran_at)}`:""]}):T==="running"?e("span",{class:"field-hint",children:"Running\u2026"}):b?e("span",{class:"field-hint",children:t==="rookery"?"Runs on the next API start.":"Runs on the next VPP connection."}):e("span",{class:"field-hint",children:"Disabled."})]}),e("form",{class:"form-grid",onSubmit:Z=>{p(Z)},children:[e("label",{class:"field field-wide checkbox-field",children:e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:b,onChange:Z=>w(Z.currentTarget.checked)}),e("span",{children:["Enable ",L," startup script"]})]})}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Script"}),e("textarea",{class:"startup-script-editor",rows:t==="rookery"?7:9,spellcheck:!1,placeholder:s,value:d,onInput:Z=>C(Z.currentTarget.value)}),e("span",{class:"field-hint",children:"The script runs with `/bin/sh` and the API process privileges. Only configure trusted commands."})]}),y?e("p",{class:"form-error field-wide",role:"alert",children:y}):null,e("div",{class:"field-wide actions",children:e("button",{class:"button primary",type:"submit",disabled:I||h,children:I?"Saving\u2026":`Save ${L} startup script`})})]}),B?e("details",{class:"startup-script-output",open:T==="failed",children:[e("summary",{children:"Last output"}),e("pre",{children:B})]}):null]})}function Ws(){return e(ge,{children:[e(Os,{}),e(qs,{kind:"rookery",title:"Rookery startup script",endpoint:"/api/startup-script",description:"Runs once per API process after Rookery starts. It is empty by default. Use it for host-level setup that should happen once when the service starts.",placeholder:`# Runs once when the Rookery API starts +# Add host or setup commands here`,spaced:!0}),e(qs,{kind:"vpp",title:"VPP startup script",endpoint:"/api/vpp-startup-script",description:"Runs every time Rookery connects to FD.io VPP, including after the connection drops or VPP restarts. It is enabled by default to assign NAT44 workers.",placeholder:`# Assign NAT44 workers +vppctl set nat workers 0`,spaced:!0})]})}function zs(t){t&&Fe(e(Ws,{}),t)}function Wt(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}var Zi="[A-Za-z0-9][A-Za-z0-9._\\-]*";function An(t,n){if(!t)return"";let[a,l]=t.trim().split("/"),s=a.split(".").map(Number);if(s.length!==4||s.some(f=>!Number.isInteger(f)||f<0||f>255)||!l)return"";let o=(s[0]<<24>>>0)+(s[1]<<16)+(s[2]<<8)+s[3]+n>>>0;return`${o>>>24&255}.${o>>>16&255}.${o>>>8&255}.${o&255}`}function Yi(t,n){if(!t)return"";let[a,l]=t.trim().split("/");if(!a||!l)return"";let s=a.split("::");if(s.length>2)return"";let o=h=>h?h.split(":"):[],f=o(s[0]),r=o(s[1]??""),b=8-f.length-r.length;if(b<0)return"";let w=[...f,...Array.from({length:b},()=>"0"),...r];if(w.some(h=>!/^[0-9a-fA-F]{1,4}$/.test(h)))return"";let d=0n;for(let h of w)d=(d<<16n)+BigInt(parseInt(h,16));d+=BigInt(n);let C=[];for(let h=0;h<8;h+=1)C.unshift((d&0xffffn).toString(16)),d>>=16n;return C.join(":")}function Nn(t){let n=An(t,1);return!n||!t?"":`${n}/${t.trim().split("/")[1]}`}function Qi(t,n,a,l){let s=t?.type??"dhcpv4",o=t?.config??{},f=n.find(b=>b.id===(t?.network_id??l??n[0]?.id)),r=Nn(f?.ipv4_cidr)||"10.0.0.1/16";return{name:t?.name??"",networkID:t?.network_id??l??n[0]?.id??0,type:s,enabled:t?t.enabled:a,gateway:o.gateway??(s==="dhcpv6"?"fd00:20::1/64":r),dns:o.dns??(s==="dhcpv6"?"2001:4860:4860::8888":"1.1.1.1"),rangeStart:o.range_start??(s==="dhcpv6"?"fd00:20::100":"10.20.0.100"),rangeEnd:o.range_end??(s==="dhcpv6"?"fd00:20::200":"10.20.0.200"),leaseTime:o.lease_time??"12h",outsideInterface:o.outside_interface??"",outsideAddress:o.outside_address??"",outsideGateway:o.outside_gateway??"",nat44Pool:o.nat44_pool??"",dnatRules:Array.isArray(o.dnat_rules)?o.dnat_rules.map(b=>({...b})):[],staticAssignments:Array.isArray(o.static_assignments)?o.static_assignments.map(b=>({...b})):[],dhcpOptions:Array.isArray(o.dhcp_options)?o.dhcp_options.map(b=>({...b})):[],dnsRecords:Array.isArray(o.dns_records)?o.dns_records.map(b=>({...b})):[],dnsForward:o.dns_forward??!1,dnsForwarders:Array.isArray(o.dns_forwarders)?o.dns_forwarders.join(", "):t?.type==="dns"?"":"1.1.1.1, 8.8.8.8",dnsVNFID:o.dns_vnf_id??0,dnsDomain:o.dns_domain??"",dnsTTL:String(o.dns_ttl??300),transport:s==="bgp"?o.transport==="tap"?"tap":"memif":o.transport==="shared"||o.transport==="dedicated"||o.transport==="tap"?o.transport:"inherit",localAS:String(o.local_as??64512),routerID:o.router_id??"10.20.0.1",peerAS:String(o.peer_as??64513),peerIP:o.peer_ip??"169.254.0.2",localIP:o.local_ip??"169.254.0.1/30",peerGateway:o.peer_gateway??"",nextHop:o.next_hop??"",multihopTTL:String(o.multihop_ttl??1),password:o.password??"",passive:o.passive??!1,targetTableID:String(o.target_table_id??0),advertisePrefixes:Array.isArray(o.advertise_prefixes)?o.advertise_prefixes.join(` +`):""}}function er(t,n){return n==="dns"?{...t,type:n,dnsRecords:[],dnsForward:!0,dnsForwarders:"1.1.1.1, 8.8.8.8"}:n==="bgp"?{...t,type:n,transport:"memif",localAS:"64512",routerID:"10.20.0.1",peerAS:"64513",peerIP:"169.254.0.2",localIP:"169.254.0.1/30",peerGateway:"",nextHop:"",multihopTTL:"1",password:"",passive:!1,targetTableID:"0",advertisePrefixes:""}:n==="dhcpv6"?{...t,type:n,transport:"tap",gateway:"fd00:20::1/64",dns:"2001:4860:4860::8888",rangeStart:"fd00:20::100",rangeEnd:"fd00:20::200",leaseTime:"12h",dnsVNFID:0,dnsDomain:"",dnsTTL:"300"}:n==="nat44"?{...t,type:n,gateway:"10.20.0.1/24",outsideAddress:"",outsideGateway:"",nat44Pool:""}:{...t,type:n,transport:"inherit",gateway:"10.20.0.1/24",dns:"1.1.1.1",rangeStart:"10.20.0.100",rangeEnd:"10.20.0.200",leaseTime:"12h",dnsVNFID:0,dnsDomain:"",dnsTTL:"300"}}function tr(t,n){let a=t.find(l=>l.name===n);return a?.default_route?"This NIC carries the host default route and cannot be used for NAT.":a?.physical===!1?"Select a wired physical NIC; virtual and VPP-managed interfaces are not valid outside uplinks.":a?.wireless?"Wireless NICs are not supported as VPP NAT44 outside uplinks; use a wired NIC.":a?`VPP will take over ${a.name} and flush its host addresses.`:"The selected NIC will be taken over by VPP; its host addresses are flushed."}function nr(t){return t==="dns"?"Local DNS server on the network service address (.2) over IPv4 and IPv6. Serve A/AAAA/CNAME/TXT/PTR records and optionally forward other names upstream.":t==="bgp"?"Embedded IPv4/IPv6 BGP speaker. Select memif for userspace/gVisor or tap for kernel TCP with TCP MD5 support.":t==="nat44"?"IPv4 NAT44 gateway. A dedicated network BVI owns the gateway; the outside NIC is taken over by VPP.":t==="dhcpv6"?"IPv6 address pool with stateful IA_NA leases on a VPP tap (shared memif is DHCPv4-only for now).":"IPv4 address pool served on the network-shared memif by default (VNF address .2)."}var ar=[{code:3,label:"Router",format:"ipv4",placeholder:"10.20.0.1",hint:"Overrides the gateway router option."},{code:6,label:"DNS servers",format:"ipv4",placeholder:"10.20.0.2, 1.1.1.1",hint:"Overrides the DNS field."},{code:15,label:"Domain name",format:"string",placeholder:"lab.test",hint:"This is the domain name, not DNS server addresses. Use code 6 for DNS servers."},{code:42,label:"NTP servers",format:"ipv4",placeholder:"10.20.0.1, 10.20.0.2"},{code:44,label:"WINS servers",format:"ipv4",placeholder:"10.20.0.10"},{code:66,label:"TFTP server name",format:"string",placeholder:"tftp.lab.test"},{code:67,label:"Bootfile name",format:"string",placeholder:"pxelinux.0"},{code:119,label:"Domain search list",format:"domain-list",placeholder:"lab.test, apps.lab.test"},{code:252,label:"WPAD URL",format:"string",placeholder:"http://wpad.lab.test/wpad.dat"}],sr=[{code:21,label:"SIP domain names",format:"domain-list",placeholder:"sip.lab.test"},{code:22,label:"SIP servers",format:"ipv6",placeholder:"fd00:20::10"},{code:23,label:"DNS recursive name servers",format:"ipv6",placeholder:"fd00:20::2",hint:"Overrides the DNS field."},{code:24,label:"Domain search list",format:"domain-list",placeholder:"lab.test, apps.lab.test"},{code:31,label:"SNTP servers",format:"ipv6",placeholder:"fd00:20::10"},{code:59,label:"Bootfile URL",format:"string",placeholder:"http://[fd00:20::1]/boot"},{code:60,label:"Bootfile parameters",format:"string",placeholder:"console=ttyS0"}];function Gs(t){return t==="dhcpv6"?sr:ar}function In(t,n){return Gs(t).find(a=>a.code===n)}function Ks({vnf:t,networks:n,vnfs:a,vppReady:l,initialNetworkID:s,onClose:o,onSaved:f}){let[r,b]=c(()=>Qi(t,n,l,s)),[w,d]=c([]),[C,h]=c(!1),[A,I]=c(!1),[m,y]=c(""),[P,L]=ke("vnf",t?.id),S=ne(null),N=ne(null),p=t!==null,T=r.type==="nat44",B=r.type==="bgp",K=r.type==="dns",Z=n.find(u=>u.id===r.networkID),ie=An(Z?.ipv4_cidr,2),x=Yi(Z?.ipv6_cidr,2),R=a.filter(u=>u.type==="dns"&&u.network_id===r.networkID),F=(()=>{let u=Z?.ipv4_cidr?.trim(),_=An(u,2),$=u?.split("/")[1];return _&&$?`${_}/${$}`:""})(),W=r.gateway||"the gateway address";G(()=>{S.current?.focus(),document.title=`${t?"Edit":"Add"} VNF \xB7 rookery`},[t]),G(()=>{if(!T)return;let u=!1;return h(!0),V("/api/host/interfaces").then(_=>{u||d(Array.isArray(_)?_:[])}).catch(()=>{u||d([])}).finally(()=>{u||h(!1)}),()=>{u=!0}},[T]);let Q=(u,_)=>{b($=>({...$,[u]:_}))},g=()=>{A||o()},oe=async u=>{if(u.preventDefault(),A||!N.current?.reportValidity())return;if(y(""),!B&&!K&&!r.gateway.trim()){y("Gateway CIDR is required.");return}if(!B&&!T&&!K&&(!r.rangeStart.trim()||!r.rangeEnd.trim())){y("Range start and end are required.");return}if(T&&!r.outsideInterface){y("Select an outside network interface.");return}if(T&&r.enabled&&!window.confirm(`Take over ${r.outsideInterface} for NAT44? -VPP will own this NIC and its host IPv4/IPv6 addresses will be flushed. The host default-route interface is rejected by the server.`))return;let _=r.advertisePrefixes.split(/[\n,]+/).map(Z=>Z.trim()).filter(Boolean),B=H?{transport:r.transport==="tap"?"tap":"memif",local_as:Number(r.localAS),router_id:r.routerID.trim(),peer_as:Number(r.peerAS),peer_ip:r.peerIP.trim(),local_ip:r.localIP.trim(),peer_gateway:r.peerGateway.trim(),next_hop:r.nextHop.trim(),multihop_ttl:Number(r.multihopTTL),password:r.transport==="tap"?r.password:"",passive:r.passive,target_table_id:Number(r.targetTableID),advertise_prefixes:_}:G?{dns_records:r.dnsRecords.map(Z=>({name:Z.name.trim(),type:Z.type,value:Z.value.trim(),ttl:Number(Z.ttl)||0})),dns_forward:r.dnsForward,dns_forwarders:r.dnsForwarders.split(/[\n,]+/).map(Z=>Z.trim()).filter(Boolean)}:T?{gateway:r.gateway.trim(),outside_interface:r.outsideInterface,outside_address:r.outsideAddress.trim(),outside_gateway:r.outsideGateway.trim(),nat44_pool:r.nat44Pool.trim(),dnat_rules:r.dnatRules.map(Z=>({protocol:Z.protocol,external_port:Number(Z.external_port),local_ip:Z.local_ip.trim(),local_port:Number(Z.local_port)}))}:{gateway:r.gateway.trim(),range_start:r.rangeStart.trim(),range_end:r.rangeEnd.trim(),dns:r.dns.trim(),lease_time:r.leaseTime.trim()||"12h",dhcp_options:r.dhcpOptions.map(Z=>({code:Number(Z.code),format:Z.format,value:Z.value.trim()})),...r.transport==="shared"||r.transport==="dedicated"||r.transport==="tap"?{transport:r.transport}:{},...r.type==="dhcpv4"?{static_assignments:r.staticAssignments.map(Z=>({mac:Z.mac.trim(),ip:Z.ip.trim()}))}:{},...r.dnsVNFID>0?{dns_vnf_id:r.dnsVNFID,dns_domain:r.dnsDomain.trim(),dns_ttl:Number(r.dnsTTL)||300}:{}},X={name:r.name.trim(),enabled:r.enabled,config:B};p||(X.network_id=r.networkID,X.type=r.type),I(!0);try{let Z=await V(p?`/api/vnfs/${t?.id}`:"/api/vnfs",{method:p?"PUT":"POST",body:JSON.stringify(X)});await Ce("vnf",Z.id,Z.name,P),f(Z)}catch(Z){g(Z instanceof Error?Z.message:String(Z))}finally{I(!1)}},E=()=>{b(c=>({...c,dnatRules:[...c.dnatRules,{protocol:"tcp",external_port:8080,local_ip:"",local_port:80}]}))},M=(c,_,B)=>{b(X=>({...X,dnatRules:X.dnatRules.map((Z,fe)=>fe===c?{...Z,[_]:B}:Z)}))},se=c=>{b(_=>({..._,dnatRules:_.dnatRules.filter((B,X)=>X!==c)}))},ce=()=>{b(c=>({...c,staticAssignments:[...c.staticAssignments,{mac:"",ip:""}]}))},v=(c,_,B)=>{b(X=>({...X,staticAssignments:X.staticAssignments.map((Z,fe)=>fe===c?{...Z,[_]:B}:Z)}))},$=c=>{b(_=>({..._,staticAssignments:_.staticAssignments.filter((B,X)=>X!==c)}))},C=()=>{b(c=>({...c,dhcpOptions:[...c.dhcpOptions,{code:0,format:"string",value:""}]}))},U=(c,_,B)=>{b(X=>({...X,dhcpOptions:X.dhcpOptions.map((Z,fe)=>fe===c?{...Z,[_]:B}:Z)}))},de=(c,_)=>{let B=Ln(r.type,_);b(X=>({...X,dhcpOptions:X.dhcpOptions.map((Z,fe)=>fe===c?{...Z,code:_,...B?{format:B.format}:{}}:Z)}))},ie=c=>{b(_=>({..._,dhcpOptions:_.dhcpOptions.filter((B,X)=>X!==c)}))},pe=()=>{b(c=>({...c,dnsRecords:[...c.dnsRecords,{name:"",type:"A",value:"",ttl:300}]}))},ee=(c,_,B)=>{b(X=>({...X,dnsRecords:X.dnsRecords.map((Z,fe)=>fe===c?{...Z,[_]:B}:Z)}))},ue=c=>{b(_=>({..._,dnsRecords:_.dnsRecords.filter((B,X)=>X!==c)}))},me=H?l?r.transport==="memif"?"Creates a memif and starts the embedded BGP speaker on gVisor. TCP MD5 is unavailable on this transport.":"Creates a VPP tap and starts the embedded BGP speaker on kernel TCP; TCP MD5 is supported.":"VPP is not ready. The BGP VNF can be created disabled and started later.":G?l?"Starts a DNS server on the network service address (.2, UDP/TCP 53) using the shared VNF memif.":"VPP is not ready. The DNS VNF can be created disabled and started later.":T?l?r.enabled?"Saving takes over the selected outside NIC and flushes its host addresses.":"The configuration is saved without taking over the outside NIC.":"VPP is not ready. Save disabled and start the NAT44 VNF later.":l?"Creates a VPP tap and starts the function immediately.":"VPP is not ready. The VNF can be created disabled and started later.";return e("div",{class:"vnf-page",children:e("form",{ref:N,class:"vnf-page-form",novalidate:!0,onSubmit:c=>{ae(c)},children:[e("header",{class:"route-page-head",children:e("div",{children:[e("button",{class:"back-link",type:"button",disabled:A,onClick:y,children:"\u2190 VNFs"}),e("p",{class:"eyebrow",children:"Network function"}),e("h2",{id:"vnf-dialog-title",children:p?`Edit ${t?.name??"VNF"}`:"Add VNF"})]})}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:S,type:"text",required:!0,maxlength:64,pattern:Zi,autocomplete:"off",spellcheck:!1,placeholder:"lab-dhcpv4",value:r.name,onInput:c=>Y("name",c.currentTarget.value)}),e("span",{class:"field-hint",children:"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Network"}),e("select",{required:!0,disabled:p,value:String(r.networkID),onChange:c=>{let _=Number(c.currentTarget.value),B=n.find(X=>X.id===_);b(X=>({...X,networkID:_,dnsVNFID:s.some(Z=>Z.id===X.dnsVNFID&&Z.type==="dns"&&Z.network_id===_)?X.dnsVNFID:0,gateway:X.type==="dhcpv6"?X.gateway:An(B?.ipv4_cidr)||X.gateway}))},children:n.map(c=>e("option",{value:String(c.id),children:[c.name," \xB7 bd ",c.bridge_domain_id]},c.id))})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Type"}),e("select",{required:!0,disabled:p,value:r.type,onChange:c=>{if(!p){let _=c.currentTarget.value;b(B=>{let X=Qi(B,_),Z=n.find(fe=>fe.id===X.networkID);return(_==="dhcpv4"||_==="nat44")&&Z?.ipv4_cidr&&(X.gateway=An(Z.ipv4_cidr)||X.gateway),X})}},children:[e("option",{value:"dhcpv4",children:"DHCPv4"}),e("option",{value:"dhcpv6",children:"DHCPv6"}),e("option",{value:"nat44",children:"NAT44"}),e("option",{value:"bgp",children:"BGP"}),e("option",{value:"dns",children:"DNS"})]}),e("span",{class:"field-hint",children:tr(r.type)})]}),!H&&!G?e("label",{class:"field",children:[e("span",{class:"field-label",children:"Gateway CIDR"}),e("input",{type:"text",required:!0,placeholder:T?"10.20.0.1/24 (network BVI gateway)":r.type==="dhcpv6"?"fd00:20::1/64":"10.20.0.1/24",autocomplete:"off",spellcheck:!1,value:r.gateway,onInput:c=>Y("gateway",c.currentTarget.value)}),e("span",{class:"field-hint",children:["Network range: ",n.find(c=>c.id===r.networkID)?.ipv4_cidr||"not set"]})]}):null]}),e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:e(Se,{labels:P,onChange:L,disabled:A})})}),!T&&!H&&!G?e(ge,{children:[e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:["DNS servers ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:r.type==="dhcpv6"?"2001:4860:4860::8888":"1.1.1.1,8.8.8.8",autocomplete:"off",spellcheck:!1,value:r.dns,onInput:c=>Y("dns",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Range start"}),e("input",{type:"text",required:!0,placeholder:r.type==="dhcpv6"?"fd00:20::100":"10.20.0.100",autocomplete:"off",spellcheck:!1,value:r.rangeStart,onInput:c=>Y("rangeStart",c.currentTarget.value)})]}),r.type==="dhcpv4"?e("label",{class:"field",children:[e("span",{class:"field-label",children:"VNF networking"}),e("select",{value:r.transport,onChange:c=>Y("transport",c.currentTarget.value),children:[e("option",{value:"inherit",children:"Inherit network default (shared)"}),e("option",{value:"shared",children:["Shared network memif",F?` \xB7 ${F}`:""]}),e("option",{value:"dedicated",children:["Dedicated memif",W?` \xB7 ${W}`:""]}),e("option",{value:"tap",children:["Tap",W?` \xB7 ${W}`:""]})]}),e("span",{class:"field-hint",children:r.transport==="shared"?`The VNF uses the shared ${F||".2 service"} address; the BVI owns the gateway (.1).`:r.transport==="dedicated"?`The VNF uses ${W}; on a NAT network the server may move it to the .2 service address.`:r.transport==="tap"?`The tap is assigned ${W}; on a NAT network the server may move it to the .2 service address.`:`Inherits the network default (${J?.vnf_transport||"shared"}). Shared uses ${F||".2"}.`})]}):null,e("label",{class:"field",children:[e("span",{class:"field-label",children:"Range end"}),e("input",{type:"text",required:!0,placeholder:r.type==="dhcpv6"?"fd00:20::200":"10.20.0.200",autocomplete:"off",spellcheck:!1,value:r.rangeEnd,onInput:c=>Y("rangeEnd",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Lease time"}),e("input",{type:"text",placeholder:"12h",autocomplete:"off",spellcheck:!1,value:r.leaseTime,onInput:c=>Y("leaseTime",c.currentTarget.value)})]})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["DHCP DNS integration ",e("em",{children:"optional"})]}),e("select",{value:String(r.dnsVNFID),onChange:c=>{let _=Number(c.currentTarget.value),B=r.type==="dhcpv6"?x:ne;b(X=>({...X,dnsVNFID:_,..._>0&&B?{dns:B}:{}}))},children:[e("option",{value:"0",children:"No DNS integration"}),R.map(c=>e("option",{value:String(c.id),children:c.name},c.id))]}),e("span",{class:"field-hint",children:R.length===0?"Create a DNS VNF on this network first to register DHCP leases.":"Active leases get A/AAAA and PTR records in the selected DNS VNF. The DNS server field is set to its service address (.2)."})]}),r.dnsVNFID>0?e(ge,{children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"DNS domain"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"lab1.test",value:r.dnsDomain,onInput:c=>Y("dnsDomain",c.currentTarget.value)}),e("span",{class:"field-hint",children:"Used when the client does not send a FQDN."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"DNS TTL (seconds)"}),e("input",{type:"number",min:1,max:86400,value:r.dnsTTL,onInput:c=>Y("dnsTTL",c.currentTarget.value)})]})]}):null]}),r.type==="dhcpv4"?e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:["Static assignments ",e("em",{children:"optional"})]}),e("span",{class:"field-hint",children:"Reserve an IPv4 address for a MAC address. Reserved addresses are excluded from the dynamic pool and can be inside or outside the DHCP range."})]}),e("button",{class:"button ghost",type:"button",onClick:ce,children:"Add assignment"})]}),e("div",{class:"acl-rule-rows",children:r.staticAssignments.map((c,_)=>e("div",{class:"acl-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"dnat-rule-title",children:["Assignment ",_+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>$(_),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field dhcp-static-field",children:[e("span",{class:"field-label",children:"MAC address"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"02:00:00:00:00:01",value:c.mac,onInput:B=>v(_,"mac",B.currentTarget.value)})]}),e("label",{class:"field dhcp-static-field",children:[e("span",{class:"field-label",children:"IP address"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,inputMode:"decimal",placeholder:"10.20.0.50",value:c.ip,onInput:B=>v(_,"ip",B.currentTarget.value)})]})]})]},_))})]})}):null,e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:["Extra DHCP options ",e("em",{children:"optional"})]}),e("span",{class:"field-hint",children:["Advertise additional options such as option 15 (domain name), 42 (NTP), 119 (domain search), or DHCPv6 option 24 (domain search). Use",e("code",{children:"string"}),", ",e("code",{children:"ipv4"}),", ",e("code",{children:"ipv6"}),", ",e("code",{children:"hex"}),",",e("code",{children:"uint8"}),", ",e("code",{children:"uint16"}),", ",e("code",{children:"uint32"}),", or",e("code",{children:"domain-list"}),". DNS server options are set through the DNS field."]})]}),e("button",{class:"button ghost",type:"button",onClick:C,children:"Add option"})]}),e("div",{class:"acl-rule-rows",children:[r.dhcpOptions.map((c,_)=>e("div",{class:"acl-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"dnat-rule-title",children:["Option ",_+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>ie(_),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field dhcp-option-code",children:[e("span",{class:"field-label",children:"Code"}),e("input",{type:"number",required:!0,min:1,max:r.type==="dhcpv6"?65535:254,list:`dhcp-option-codes-${r.type}`,value:c.code||"",onInput:B=>de(_,Number(B.currentTarget.value))}),(()=>{let B=Ln(r.type,c.code);return e("span",{class:"field-hint",children:B?`${B.label}${B.hint?` \u2014 ${B.hint}`:""}`:"Custom option code"})})()]}),e("label",{class:"field dhcp-option-format",children:[e("span",{class:"field-label",children:"Format"}),e("select",{value:c.format,onChange:B=>U(_,"format",B.currentTarget.value),children:[e("option",{value:"string",children:"string"}),e("option",{value:"ipv4",children:"ipv4"}),e("option",{value:"ipv6",children:"ipv6"}),e("option",{value:"hex",children:"hex"}),e("option",{value:"uint8",children:"uint8"}),e("option",{value:"uint16",children:"uint16"}),e("option",{value:"uint32",children:"uint32"}),e("option",{value:"domain-list",children:"domain-list"})]})]}),e("label",{class:"field dhcp-option-value",children:[e("span",{class:"field-label",children:"Value"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:Ln(r.type,c.code)?.placeholder??(c.format==="ipv4"?"10.20.0.1, 10.20.0.2":c.format==="ipv6"?"fd00:20::1":c.format==="domain-list"?"lab.test, apps.lab.test":c.format==="hex"?"01:02:ff":"lab.test"),value:c.value,onInput:B=>U(_,"value",B.currentTarget.value)})]})]})]},_)),r.dhcpOptions.length===0?e("p",{class:"field-hint",children:"No extra options. Example: code 15, format string, value lab.test advertises the domain name. DNS and router options override the DNS field and gateway."}):null]}),e("datalist",{id:`dhcp-option-codes-${r.type}`,children:za(r.type).map(c=>e("option",{value:c.code,label:`${c.label} (${c.format})`}))})]})})]}):G?e(ge,{children:[e("p",{class:"field-hint",children:["DHCPv4 clients can use ",e("code",{children:ne||"the IPv4 .2 service address"}),"; DHCPv6 clients can use ",e("code",{children:x||"the IPv6 .2 service address"}),". Set that address in the DHCP VNF's DNS field (or configure it directly on clients)."]}),e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:"DNS records"}),e("span",{class:"field-hint",children:"Names are served authoritatively. Use a fully qualified name such as vm1.lab1.test; a leading *. wildcard is supported. Types: A, AAAA, CNAME, TXT, PTR."})]}),e("button",{class:"button ghost",type:"button",onClick:pe,children:"Add record"})]}),e("div",{class:"acl-rule-rows",children:[r.dnsRecords.map((c,_)=>e("div",{class:"acl-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"dnat-rule-title",children:["Record ",_+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>ue(_),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Name"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"vm1.lab1.test",value:c.name,onInput:B=>ee(_,"name",B.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Type"}),e("select",{value:c.type,onChange:B=>ee(_,"type",B.currentTarget.value),children:[e("option",{value:"A",children:"A"}),e("option",{value:"AAAA",children:"AAAA"}),e("option",{value:"CNAME",children:"CNAME"}),e("option",{value:"TXT",children:"TXT"}),e("option",{value:"PTR",children:"PTR"})]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Value"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:c.type==="A"?"10.20.0.50":c.type==="AAAA"?"fd00:20::50":c.type==="CNAME"||c.type==="PTR"?"vm1.lab1.test":"text",value:c.value,onInput:B=>ee(_,"value",B.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"TTL"}),e("input",{type:"number",min:0,max:86400,placeholder:"300",value:c.ttl??300,onInput:B=>ee(_,"ttl",Number(B.currentTarget.value)||0)})]})]})]},_)),r.dnsRecords.length===0?e("p",{class:"field-hint",children:"No local records yet. Add one, or use this VNF only as a forwarder."}):null]})]})}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:r.dnsForward,onChange:c=>Y("dnsForward",c.currentTarget.checked)}),e("span",{children:"Forward names without a local record to upstream DNS servers"})]}),e("span",{class:"field-hint",children:"When disabled, unknown names receive NXDOMAIN. Local records always take precedence."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Upstream DNS servers"}),e("input",{type:"text",disabled:!r.dnsForward,autocomplete:"off",spellcheck:!1,placeholder:"1.1.1.1, 8.8.8.8:53",value:r.dnsForwarders,onInput:c=>Y("dnsForwarders",c.currentTarget.value)}),e("span",{class:"field-hint",children:"Comma-separated IPv4/IPv6 addresses or IP:port. Leave empty to use /etc/resolv.conf on the server."})]})]})]}):H?e(ge,{children:e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Transport"}),e("select",{value:r.transport,onChange:c=>Y("transport",c.currentTarget.value),children:[e("option",{value:"memif",children:"memif + gVisor"}),e("option",{value:"tap",children:"VPP tap + kernel TCP"})]}),e("span",{class:"field-hint",children:"TCP MD5 passwords require the tap transport; memif/gVisor does not support TCP MD5."})]}),e("label",{class:"field checkbox-field",children:e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:r.passive,onChange:c=>Y("passive",c.currentTarget.checked)}),e("span",{children:"Passive mode \u2014 wait for the peer to connect"})]})}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Local AS"}),e("input",{type:"number",required:!0,min:1,value:r.localAS,onInput:c=>Y("localAS",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Router ID"}),e("input",{type:"text",required:!0,placeholder:"10.20.0.1",autocomplete:"off",value:r.routerID,onInput:c=>Y("routerID",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Peer AS"}),e("input",{type:"number",required:!0,min:1,value:r.peerAS,onInput:c=>Y("peerAS",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Peer IPv4/IPv6 address"}),e("input",{type:"text",required:!0,placeholder:"169.254.0.2",autocomplete:"off",value:r.peerIP,onInput:c=>Y("peerIP",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Local IPv4/IPv6 CIDR"}),e("input",{type:"text",required:!0,placeholder:"169.254.0.1/30",autocomplete:"off",value:r.localIP,onInput:c=>Y("localIP",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Peer gateway ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"169.254.0.2 (for routed/multihop peers)",autocomplete:"off",value:r.peerGateway,onInput:c=>Y("peerGateway",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Advertised next hop ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"defaults to local_ip",autocomplete:"off",value:r.nextHop,onInput:c=>Y("nextHop",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Multihop TTL"}),e("input",{type:"number",required:!0,min:1,max:255,value:r.multihopTTL,onInput:c=>Y("multihopTTL",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Target VPP table ID"}),e("input",{type:"number",required:!0,min:0,value:r.targetTableID,onInput:c=>Y("targetTableID",c.currentTarget.value)}),e("span",{class:"field-hint",children:"0 is the default VRF; received IPv4 routes are installed here."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["TCP MD5 password ",e("em",{children:"tap only"})]}),e("input",{type:"password",disabled:r.transport==="memif",placeholder:r.transport==="memif"?"Unavailable on memif":"optional",autocomplete:"new-password",value:r.password,onInput:c=>Y("password",c.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Advertised IPv4 prefixes ",e("em",{children:"optional"})]}),e("textarea",{rows:4,placeholder:`10.10.0.0/16 -10.20.0.0/24`,spellcheck:!1,value:r.advertisePrefixes,onInput:c=>Y("advertisePrefixes",c.currentTarget.value)}),e("span",{class:"field-hint",children:"One IPv4 CIDR per line. These prefixes are advertised to the peer."})]})]})}):e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Outside NIC"}),e("select",{required:!0,value:r.outsideInterface,onChange:c=>Y("outsideInterface",c.currentTarget.value),children:[e("option",{value:"",disabled:!0,children:k?"Loading host interfaces\u2026":"Select a wired physical NIC\u2026"}),w.map(c=>{let _=c.default_route||c.physical===!1||c.wireless,B=Array.isArray(c.addresses)&&c.addresses.length?` \u2014 ${c.addresses.join(", ")}`:"",X=c.default_route?" (default route \u2014 blocked)":c.physical===!1?" (virtual \u2014 not selectable)":c.wireless?" (wireless \u2014 not supported)":"";return e("option",{value:c.name,disabled:_,children:[c.name,B,X]},c.name)}),r.outsideInterface&&!w.some(c=>c.name===r.outsideInterface)?e("option",{value:r.outsideInterface,children:[r.outsideInterface," (not currently present)"]}):null]}),e("span",{class:"field-hint",children:er(w,r.outsideInterface)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Outside address ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"empty = VPP DHCP client",autocomplete:"off",spellcheck:!1,value:r.outsideAddress,onInput:c=>Y("outsideAddress",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Outside gateway ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"198.51.100.1",autocomplete:"off",spellcheck:!1,value:r.outsideGateway,onInput:c=>Y("outsideGateway",c.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["NAT44 pool ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"203.0.113.10-203.0.113.20",autocomplete:"off",spellcheck:!1,value:r.nat44Pool,onInput:c=>Y("nat44Pool",c.currentTarget.value)})]}),e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:["DNAT / port forwarding ",e("em",{children:"optional"})]}),e("span",{class:"field-hint",children:"Expose services inside this network on the NAT44 outside address. Saving restarts NAT44."})]}),e("button",{class:"button ghost",type:"button",onClick:E,children:"Add rule"})]}),e("div",{class:"acl-rule-rows",children:r.dnatRules.map((c,_)=>e("div",{class:"acl-rule dnat-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"dnat-rule-title",children:["Rule ",_+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>se(_),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field dnat-field-protocol",children:[e("span",{class:"field-label",children:"Protocol"}),e("select",{value:c.protocol,onChange:B=>M(_,"protocol",B.currentTarget.value),children:[e("option",{value:"tcp",children:"TCP"}),e("option",{value:"udp",children:"UDP"})]})]}),e("label",{class:"field dnat-field-external-port",children:[e("span",{class:"field-label",children:"External port"}),e("input",{type:"number",min:1,max:65535,step:1,required:!0,inputMode:"numeric",placeholder:"8080",value:c.external_port||"",onInput:B=>M(_,"external_port",Number(B.currentTarget.value))})]}),e("label",{class:"field dnat-field-local-ip",children:[e("span",{class:"field-label",children:"Internal address"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,inputMode:"decimal",placeholder:"10.20.0.100",value:c.local_ip,onInput:B=>M(_,"local_ip",B.currentTarget.value)})]}),e("label",{class:"field dnat-field-local-port",children:[e("span",{class:"field-label",children:"Internal port"}),e("input",{type:"number",min:1,max:65535,step:1,required:!0,inputMode:"numeric",placeholder:"80",value:c.local_port||"",onInput:B=>M(_,"local_port",Number(B.currentTarget.value))})]})]})]},_))})]})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:r.enabled,disabled:!p&&!l,onChange:c=>Y("enabled",c.currentTarget.checked)}),e("span",{children:"Enable this VNF"})]}),e("span",{class:"field-hint",children:me})]}),m?e("p",{class:"form-error",role:"alert",children:m}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:A,onClick:y,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:A,children:A?"Saving\u2026":p?"Save VNF":"Add VNF"})]})]})})}function ar(t){return t.replace(/^SESSION_STATE_/,"").toLowerCase()}function Ka({vnf:t,onClose:n}){let[s,l]=d(null),[a,o]=d(""),[f,r]=d(!1),b=te(null),w=async()=>{r(!0);try{let h=await V(`/api/vnfs/${t.id}/bgp`);l(h),o("")}catch(h){o(h instanceof Error?h.message:String(h))}finally{r(!1)}};z(()=>{b.current?.showModal(),w()},[]);let u=s?.session_state||"",k=s?s.established?"established":s.running?ar(u)||"starting":"not running":"loading";return e("dialog",{ref:b,class:"vnf-bgp-dialog","aria-labelledby":"vnf-bgp-title",onClose:n,onClick:h=>{h.target===h.currentTarget&&b.current?.close()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"BGP monitor"}),e("h2",{id:"vnf-bgp-title",children:["BGP status \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>b.current?.close(),children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.network_name||"network"," \xB7 local AS ",t.config.local_as||"\u2014"," \xB7 peer AS"," ",t.config.peer_as||"\u2014"]}),a?e("div",{class:"banner",role:"alert",children:a}):null,s?e(ge,{children:[e("div",{class:"bgp-summary",children:[e("span",{class:"status-badge","data-status":s.established?"running":s.running?"degraded":"disabled",children:k}),e("span",{class:"bgp-peer",children:["peer ",s.peer_address||t.config.peer_ip||"\u2014",s.peer_as?` \xB7 AS${s.peer_as}`:""]}),e("span",{class:"bgp-local",children:["local AS",s.local_as||"\u2014"]})]}),e("h3",{class:"bgp-section-title",children:"Advertised prefixes"}),e("div",{class:"table-wrap",children:s.advertised&&s.advertised.length>0?e("table",{children:[e("thead",{children:e("tr",{children:e("th",{scope:"col",children:"Prefix"})})}),e("tbody",{children:s.advertised.map(h=>e("tr",{children:e("td",{class:"mono",children:h})},h))})]}):e("div",{class:"empty",children:e("p",{children:"No advertised prefixes."})})}),e("h3",{class:"bgp-section-title",children:"Received prefixes"}),e("div",{class:"table-wrap",children:s.received&&s.received.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Prefix"}),e("th",{scope:"col",children:"Next hop"}),e("th",{scope:"col",children:"AS path"})]})}),e("tbody",{children:s.received.map(h=>e("tr",{children:[e("td",{class:"mono",children:h.prefix}),e("td",{class:"mono",children:h.next_hop||"\u2014"}),e("td",{class:"mono",children:h.as_path&&h.as_path.length>0?h.as_path.join(" "):"\u2014"})]},h.prefix))})]}):e("div",{class:"empty",children:e("p",{children:"No received prefixes."})})})]}):e("p",{class:"dialog-lede",children:f?"Loading\u2026":"BGP runtime not running."}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:f,onClick:()=>{w()},children:f?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>b.current?.close(),children:"Close"})]})]})}function or(t,n){return t==="static"?"static":t==="dhcp"?n?`DHCP #${n}`:"DHCP":t||"\u2014"}function Ja({vnf:t,onClose:n}){let[s,l]=d(null),[a,o]=d(""),[f,r]=d(!1),b=te(null),w=async()=>{r(!0);try{let k=await V(`/api/vnfs/${t.id}/dns-records`);l(k),o("")}catch(k){o(k instanceof Error?k.message:String(k))}finally{r(!1)}};z(()=>{b.current?.showModal(),w()},[]);let u=s?.records||[];return e("dialog",{ref:b,class:"vnf-bgp-dialog","aria-labelledby":"vnf-dns-records-title",onClose:n,onClick:k=>{k.target===k.currentTarget&&b.current?.close()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"DNS records"}),e("h2",{id:"vnf-dns-records-title",children:["DNS records \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>b.current?.close(),children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.network_name||"network"," \xB7 ",u.length," record",u.length===1?"":"s"," \xB7 static and DHCP-derived"]}),a?e("div",{class:"banner",role:"alert",children:a}):null,s?.dynamic_error?e("div",{class:"banner",role:"alert",children:["Dynamic records unavailable: ",s.dynamic_error]}):null,e("div",{class:"table-wrap",children:u.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Type"}),e("th",{scope:"col",children:"Value"}),e("th",{scope:"col",children:"TTL"}),e("th",{scope:"col",children:"Source"})]})}),e("tbody",{children:u.map((k,h)=>e("tr",{children:[e("td",{class:"mono",children:k.name}),e("td",{class:"mono",children:k.type}),e("td",{class:"mono",children:k.value}),e("td",{class:"mono",children:k.ttl||"\u2014"}),e("td",{children:or(k.source,k.source_vnf_id)})]},`${k.name}-${k.type}-${k.value}-${h}`))})]}):e("div",{class:"empty",children:e("p",{children:f?"Loading records\u2026":"No DNS records."})})}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:f,onClick:()=>{w()},children:f?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>b.current?.close(),children:"Close"})]})]})}function Xa({vnf:t,onClose:n}){let[s,l]=d([]),[a,o]=d(""),[f,r]=d(!1),b=te(null),w=async()=>{r(!0);try{let u=await V(`/api/vnfs/${t.id}/leases`);l(Array.isArray(u.leases)?u.leases:[]),o("")}catch(u){o(u instanceof Error?u.message:String(u))}finally{r(!1)}};return z(()=>{b.current?.showModal(),w()},[]),e("dialog",{ref:b,class:"vnf-leases-dialog","aria-labelledby":"vnf-leases-title",onClose:n,onClick:u=>{u.target===u.currentTarget&&b.current?.close()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Lease monitor"}),e("h2",{id:"vnf-leases-title",children:["Leases \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>b.current?.close(),children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.type==="dhcpv6"?"DHCPv6":"DHCPv4"," \xB7 ",t.network_name||"network"]}),a?e("div",{class:"banner",role:"alert",children:a}):null,e("div",{class:"table-wrap",children:s.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Client ID"}),e("th",{scope:"col",children:"Address"}),e("th",{scope:"col",children:"Hostname"}),e("th",{scope:"col",children:"Expires"}),e("th",{scope:"col",children:"Active"})]})}),e("tbody",{children:s.map(u=>e("tr",{children:[e("td",{class:"mono",children:u.client_id||"\u2014"}),e("td",{class:"mono",children:u.address||"\u2014"}),e("td",{children:u.hostname||"\u2014"}),e("td",{children:Wt(u.expires_at)}),e("td",{children:e("span",{class:"status-badge","data-status":u.active?"running":"disabled",children:u.active?"active":"expired"})})]},`${u.client_id}-${u.address}`))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"No leases yet"}),e("p",{children:"Clients will appear here after they receive an address from this VNF."})]})}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:f,onClick:()=>{w()},children:f?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>b.current?.close(),children:"Close"})]})]})}function Za({vnf:t,onClose:n}){let[s,l]=d([]),[a,o]=d(""),[f,r]=d(!1),b=te(null),w=async()=>{r(!0);try{let u=await V(`/api/vnfs/${t.id}/sessions`);l(Array.isArray(u.sessions)?u.sessions:[]),o("")}catch(u){o(u instanceof Error?u.message:String(u))}finally{r(!1)}};return z(()=>{b.current?.showModal(),w()},[]),e("dialog",{ref:b,class:"vnf-sessions-dialog","aria-labelledby":"vnf-sessions-title",onClose:n,onClick:u=>{u.target===u.currentTarget&&b.current?.close()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"NAT monitor"}),e("h2",{id:"vnf-sessions-title",children:["NAT44 sessions \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>b.current?.close(),children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.network_name||"network"," \xB7 outside ",t.config.outside_interface||"\u2014"]}),a?e("div",{class:"banner",role:"alert",children:a}):null,e("pre",{class:"nat-sessions",children:s.length>0?s.join(` -`):f?"Loading\u2026":"No active NAT44 sessions."}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:f,onClick:()=>{w()},children:f?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>b.current?.close(),children:"Close"})]})]})}function ja(){let[t,n]=d([]),[s,l]=d([]),[a,o]=d(!1),[f,r]=d(!0),[b,w]=d(""),[u,k]=d(null),[h,A]=d(null),[I,m]=d(null),[g,P]=d(null),[L,S]=d(null),N=Ne("vnf"),p=ze();z(()=>{if(p.segments[0]!=="vnfs"){k(null),A(null),m(null),P(null),S(null);return}let E=p.segments[1];if(E==="new"){let M=Number(p.query.get("network"))||void 0;A(null),m(null),P(null),S(null),k({vnf:null,networkID:M});return}if(E&&E!=="new"){let M=t.find(ce=>String(ce.id)===E),se=p.segments[2];M&&(k(se==="edit"?{vnf:M}:null),A(se==="leases"?M:null),m(se==="sessions"?M:null),P(se==="bgp"?M:null),S(se==="dns"?M:null));return}k(null),A(null),m(null),P(null),S(null)},[p.path,p.search,t]);let T=E=>{n(E),window.dispatchEvent(new CustomEvent("rookery:vnfs-changed",{detail:E}))},H=async(E=!0)=>{try{let M=await V("/api/vnfs");T(Array.isArray(M)?M:[]),E&&w("")}catch(M){E&&w(M instanceof Error?M.message:String(M))}},G=async()=>{try{let E=await V("/api/networks");l(Array.isArray(E)?E:[])}catch{l([])}},J=async()=>{try{let E=await V("/api/status");o(!!E.vpp?.ready)}catch{o(!1)}},ne=async()=>{r(!0),await Promise.allSettled([H(),G(),J()]),r(!1)};z(()=>{ne();let E=C=>{let U=C.detail;Array.isArray(U)?l(U):G(),H(!1)},M=C=>{o(!!C.detail)},se=C=>{let U=C.detail;Array.isArray(U)&&n(U)},ce=C=>{let U=C.detail?.networkId;Q(U?`/vnfs/new?network=${U}`:"/vnfs/new")},v=()=>{G()},$=C=>{C.detail!=="vnfs"&&(k(null),A(null),m(null),S(null))};return window.addEventListener("rookery:networks-changed",E),window.addEventListener("rookery:vpp-ready",M),window.addEventListener("rookery:vnfs-data",se),window.addEventListener("rookery:add-vnf",ce),window.addEventListener("rookery:refresh-networks",v),window.addEventListener("rookery:tab-changed",$),()=>{window.removeEventListener("rookery:networks-changed",E),window.removeEventListener("rookery:vpp-ready",M),window.removeEventListener("rookery:vnfs-data",se),window.removeEventListener("rookery:add-vnf",ce),window.removeEventListener("rookery:refresh-networks",v),window.removeEventListener("rookery:tab-changed",$)}},[]),z(()=>{let E=document.getElementById("vnf-tab-count");E&&(E.textContent=String(t.length))},[t.length]);let x=()=>{window.dispatchEvent(new CustomEvent("rookery:refresh-networks"))},R=E=>{k(null),Q("/vnfs",!0),w(""),oe(`${u?.vnf?"Saved":"Added"} VNF ${E.name}`),H(!1),(E.type==="nat44"||E.type==="dns")&&x()},F=async E=>{try{await V(`/api/vnfs/${E.id}/restart`,{method:"POST"}),w(""),oe(`Started VNF ${E.name}`),H(!1)}catch(M){let se=M instanceof Error?M.message:String(M);w(`Could not start ${E.name}: ${se}`),M instanceof Ze&&M.status===503&&J()}},W=async E=>{if(window.confirm(E.type==="nat44"?`Delete VNF "${E.name}"? +VPP will own this NIC and its host IPv4/IPv6 addresses will be flushed. The host default-route interface is rejected by the server.`))return;let _=r.advertisePrefixes.split(/[\n,]+/).map(J=>J.trim()).filter(Boolean),$=B?{transport:r.transport==="tap"?"tap":"memif",local_as:Number(r.localAS),router_id:r.routerID.trim(),peer_as:Number(r.peerAS),peer_ip:r.peerIP.trim(),local_ip:r.localIP.trim(),peer_gateway:r.peerGateway.trim(),next_hop:r.nextHop.trim(),multihop_ttl:Number(r.multihopTTL),password:r.transport==="tap"?r.password:"",passive:r.passive,target_table_id:Number(r.targetTableID),advertise_prefixes:_}:K?{dns_records:r.dnsRecords.map(J=>({name:J.name.trim(),type:J.type,value:J.value.trim(),ttl:Number(J.ttl)||0})),dns_forward:r.dnsForward,dns_forwarders:r.dnsForwarders.split(/[\n,]+/).map(J=>J.trim()).filter(Boolean)}:T?{gateway:r.gateway.trim(),outside_interface:r.outsideInterface,outside_address:r.outsideAddress.trim(),outside_gateway:r.outsideGateway.trim(),nat44_pool:r.nat44Pool.trim(),dnat_rules:r.dnatRules.map(J=>({protocol:J.protocol,external_port:Number(J.external_port),local_ip:J.local_ip.trim(),local_port:Number(J.local_port)}))}:{gateway:r.gateway.trim(),range_start:r.rangeStart.trim(),range_end:r.rangeEnd.trim(),dns:r.dns.trim(),lease_time:r.leaseTime.trim()||"12h",dhcp_options:r.dhcpOptions.map(J=>({code:Number(J.code),format:J.format,value:J.value.trim()})),...r.transport==="shared"||r.transport==="dedicated"||r.transport==="tap"?{transport:r.transport}:{},...r.type==="dhcpv4"?{static_assignments:r.staticAssignments.map(J=>({mac:J.mac.trim(),ip:J.ip.trim()}))}:{},...r.dnsVNFID>0?{dns_vnf_id:r.dnsVNFID,dns_domain:r.dnsDomain.trim(),dns_ttl:Number(r.dnsTTL)||300}:{}},j={name:r.name.trim(),enabled:r.enabled,config:$};p||(j.network_id=r.networkID,j.type=r.type),I(!0);try{let J=await V(p?`/api/vnfs/${t?.id}`:"/api/vnfs",{method:p?"PUT":"POST",body:JSON.stringify(j)});await Ce("vnf",J.id,J.name,P),f(J)}catch(J){y(J instanceof Error?J.message:String(J))}finally{I(!1)}},E=()=>{b(u=>({...u,dnatRules:[...u.dnatRules,{protocol:"tcp",external_port:8080,local_ip:"",local_port:80}]}))},z=(u,_,$)=>{b(j=>({...j,dnatRules:j.dnatRules.map((J,pe)=>pe===u?{...J,[_]:$}:J)}))},re=u=>{b(_=>({..._,dnatRules:_.dnatRules.filter(($,j)=>j!==u)}))},ue=()=>{b(u=>({...u,staticAssignments:[...u.staticAssignments,{mac:"",ip:""}]}))},v=(u,_,$)=>{b(j=>({...j,staticAssignments:j.staticAssignments.map((J,pe)=>pe===u?{...J,[_]:$}:J)}))},M=u=>{b(_=>({..._,staticAssignments:_.staticAssignments.filter(($,j)=>j!==u)}))},k=()=>{b(u=>({...u,dhcpOptions:[...u.dhcpOptions,{code:0,format:"string",value:""}]}))},O=(u,_,$)=>{b(j=>({...j,dhcpOptions:j.dhcpOptions.map((J,pe)=>pe===u?{...J,[_]:$}:J)}))},le=(u,_)=>{let $=In(r.type,_);b(j=>({...j,dhcpOptions:j.dhcpOptions.map((J,pe)=>pe===u?{...J,code:_,...$?{format:$.format}:{}}:J)}))},se=u=>{b(_=>({..._,dhcpOptions:_.dhcpOptions.filter(($,j)=>j!==u)}))},fe=()=>{b(u=>({...u,dnsRecords:[...u.dnsRecords,{name:"",type:"A",value:"",ttl:300}]}))},ee=(u,_,$)=>{b(j=>({...j,dnsRecords:j.dnsRecords.map((J,pe)=>pe===u?{...J,[_]:$}:J)}))},de=u=>{b(_=>({..._,dnsRecords:_.dnsRecords.filter(($,j)=>j!==u)}))},me=B?l?r.transport==="memif"?"Creates a memif and starts the embedded BGP speaker on gVisor. TCP MD5 is unavailable on this transport.":"Creates a VPP tap and starts the embedded BGP speaker on kernel TCP; TCP MD5 is supported.":"VPP is not ready. The BGP VNF can be created disabled and started later.":K?l?"Starts a DNS server on the network service address (.2, UDP/TCP 53) using the shared VNF memif.":"VPP is not ready. The DNS VNF can be created disabled and started later.":T?l?r.enabled?"Saving takes over the selected outside NIC and flushes its host addresses.":"The configuration is saved without taking over the outside NIC.":"VPP is not ready. Save disabled and start the NAT44 VNF later.":l?"Creates a VPP tap and starts the function immediately.":"VPP is not ready. The VNF can be created disabled and started later.";return e("div",{class:"vnf-page",children:e("form",{ref:N,class:"vnf-page-form",novalidate:!0,onSubmit:u=>{oe(u)},children:[e("header",{class:"route-page-head",children:e("div",{children:[e("button",{class:"back-link",type:"button",disabled:A,onClick:g,children:"\u2190 VNFs"}),e("p",{class:"eyebrow",children:"Network function"}),e("h2",{id:"vnf-dialog-title",children:p?`Edit ${t?.name??"VNF"}`:"Add VNF"})]})}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:S,type:"text",required:!0,maxlength:64,pattern:Zi,autocomplete:"off",spellcheck:!1,placeholder:"lab-dhcpv4",value:r.name,onInput:u=>Q("name",u.currentTarget.value)}),e("span",{class:"field-hint",children:"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Network"}),e("select",{required:!0,disabled:p,value:String(r.networkID),onChange:u=>{let _=Number(u.currentTarget.value),$=n.find(j=>j.id===_);b(j=>({...j,networkID:_,dnsVNFID:a.some(J=>J.id===j.dnsVNFID&&J.type==="dns"&&J.network_id===_)?j.dnsVNFID:0,gateway:j.type==="dhcpv6"?j.gateway:Nn($?.ipv4_cidr)||j.gateway}))},children:n.map(u=>e("option",{value:String(u.id),children:[u.name," \xB7 bd ",u.bridge_domain_id]},u.id))})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Type"}),e("select",{required:!0,disabled:p,value:r.type,onChange:u=>{if(!p){let _=u.currentTarget.value;b($=>{let j=er($,_),J=n.find(pe=>pe.id===j.networkID);return(_==="dhcpv4"||_==="nat44")&&J?.ipv4_cidr&&(j.gateway=Nn(J.ipv4_cidr)||j.gateway),j})}},children:[e("option",{value:"dhcpv4",children:"DHCPv4"}),e("option",{value:"dhcpv6",children:"DHCPv6"}),e("option",{value:"nat44",children:"NAT44"}),e("option",{value:"bgp",children:"BGP"}),e("option",{value:"dns",children:"DNS"})]}),e("span",{class:"field-hint",children:nr(r.type)})]}),!B&&!K?e("label",{class:"field",children:[e("span",{class:"field-label",children:"Gateway CIDR"}),e("input",{type:"text",required:!0,placeholder:T?"10.20.0.1/24 (network BVI gateway)":r.type==="dhcpv6"?"fd00:20::1/64":"10.20.0.1/24",autocomplete:"off",spellcheck:!1,value:r.gateway,onInput:u=>Q("gateway",u.currentTarget.value)}),e("span",{class:"field-hint",children:["Network range: ",n.find(u=>u.id===r.networkID)?.ipv4_cidr||"not set"]})]}):null]}),e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:e(Se,{labels:P,onChange:L,disabled:A})})}),!T&&!B&&!K?e(ge,{children:[e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:["DNS servers ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:r.type==="dhcpv6"?"2001:4860:4860::8888":"1.1.1.1,8.8.8.8",autocomplete:"off",spellcheck:!1,value:r.dns,onInput:u=>Q("dns",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Range start"}),e("input",{type:"text",required:!0,placeholder:r.type==="dhcpv6"?"fd00:20::100":"10.20.0.100",autocomplete:"off",spellcheck:!1,value:r.rangeStart,onInput:u=>Q("rangeStart",u.currentTarget.value)})]}),r.type==="dhcpv4"?e("label",{class:"field",children:[e("span",{class:"field-label",children:"VNF networking"}),e("select",{value:r.transport,onChange:u=>Q("transport",u.currentTarget.value),children:[e("option",{value:"inherit",children:"Inherit network default (shared)"}),e("option",{value:"shared",children:["Shared network memif",F?` \xB7 ${F}`:""]}),e("option",{value:"dedicated",children:["Dedicated memif",W?` \xB7 ${W}`:""]}),e("option",{value:"tap",children:["Tap",W?` \xB7 ${W}`:""]})]}),e("span",{class:"field-hint",children:r.transport==="shared"?`The VNF uses the shared ${F||".2 service"} address; the BVI owns the gateway (.1).`:r.transport==="dedicated"?`The VNF uses ${W}; on a NAT network the server may move it to the .2 service address.`:r.transport==="tap"?`The tap is assigned ${W}; on a NAT network the server may move it to the .2 service address.`:`Inherits the network default (${Z?.vnf_transport||"shared"}). Shared uses ${F||".2"}.`})]}):null,e("label",{class:"field",children:[e("span",{class:"field-label",children:"Range end"}),e("input",{type:"text",required:!0,placeholder:r.type==="dhcpv6"?"fd00:20::200":"10.20.0.200",autocomplete:"off",spellcheck:!1,value:r.rangeEnd,onInput:u=>Q("rangeEnd",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Lease time"}),e("input",{type:"text",placeholder:"12h",autocomplete:"off",spellcheck:!1,value:r.leaseTime,onInput:u=>Q("leaseTime",u.currentTarget.value)})]})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["DHCP DNS integration ",e("em",{children:"optional"})]}),e("select",{value:String(r.dnsVNFID),onChange:u=>{let _=Number(u.currentTarget.value),$=r.type==="dhcpv6"?x:ie;b(j=>({...j,dnsVNFID:_,..._>0&&$?{dns:$}:{}}))},children:[e("option",{value:"0",children:"No DNS integration"}),R.map(u=>e("option",{value:String(u.id),children:u.name},u.id))]}),e("span",{class:"field-hint",children:R.length===0?"Create a DNS VNF on this network first to register DHCP leases.":"Active leases get A/AAAA and PTR records in the selected DNS VNF. The DNS server field is set to its service address (.2)."})]}),r.dnsVNFID>0?e(ge,{children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"DNS domain"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"lab1.test",value:r.dnsDomain,onInput:u=>Q("dnsDomain",u.currentTarget.value)}),e("span",{class:"field-hint",children:"Used when the client does not send a FQDN."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"DNS TTL (seconds)"}),e("input",{type:"number",min:1,max:86400,value:r.dnsTTL,onInput:u=>Q("dnsTTL",u.currentTarget.value)})]})]}):null]}),r.type==="dhcpv4"?e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:["Static assignments ",e("em",{children:"optional"})]}),e("span",{class:"field-hint",children:"Reserve an IPv4 address for a MAC address. Reserved addresses are excluded from the dynamic pool and can be inside or outside the DHCP range."})]}),e("button",{class:"button ghost",type:"button",onClick:ue,children:"Add assignment"})]}),e("div",{class:"acl-rule-rows",children:r.staticAssignments.map((u,_)=>e("div",{class:"acl-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"dnat-rule-title",children:["Assignment ",_+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>M(_),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field dhcp-static-field",children:[e("span",{class:"field-label",children:"MAC address"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"02:00:00:00:00:01",value:u.mac,onInput:$=>v(_,"mac",$.currentTarget.value)})]}),e("label",{class:"field dhcp-static-field",children:[e("span",{class:"field-label",children:"IP address"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,inputMode:"decimal",placeholder:"10.20.0.50",value:u.ip,onInput:$=>v(_,"ip",$.currentTarget.value)})]})]})]},_))})]})}):null,e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:["Extra DHCP options ",e("em",{children:"optional"})]}),e("span",{class:"field-hint",children:["Advertise additional options such as option 15 (domain name), 42 (NTP), 119 (domain search), or DHCPv6 option 24 (domain search). Use",e("code",{children:"string"}),", ",e("code",{children:"ipv4"}),", ",e("code",{children:"ipv6"}),", ",e("code",{children:"hex"}),",",e("code",{children:"uint8"}),", ",e("code",{children:"uint16"}),", ",e("code",{children:"uint32"}),", or",e("code",{children:"domain-list"}),". DNS server options are set through the DNS field."]})]}),e("button",{class:"button ghost",type:"button",onClick:k,children:"Add option"})]}),e("div",{class:"acl-rule-rows",children:[r.dhcpOptions.map((u,_)=>e("div",{class:"acl-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"dnat-rule-title",children:["Option ",_+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>se(_),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field dhcp-option-code",children:[e("span",{class:"field-label",children:"Code"}),e("input",{type:"number",required:!0,min:1,max:r.type==="dhcpv6"?65535:254,list:`dhcp-option-codes-${r.type}`,value:u.code||"",onInput:$=>le(_,Number($.currentTarget.value))}),(()=>{let $=In(r.type,u.code);return e("span",{class:"field-hint",children:$?`${$.label}${$.hint?` \u2014 ${$.hint}`:""}`:"Custom option code"})})()]}),e("label",{class:"field dhcp-option-format",children:[e("span",{class:"field-label",children:"Format"}),e("select",{value:u.format,onChange:$=>O(_,"format",$.currentTarget.value),children:[e("option",{value:"string",children:"string"}),e("option",{value:"ipv4",children:"ipv4"}),e("option",{value:"ipv6",children:"ipv6"}),e("option",{value:"hex",children:"hex"}),e("option",{value:"uint8",children:"uint8"}),e("option",{value:"uint16",children:"uint16"}),e("option",{value:"uint32",children:"uint32"}),e("option",{value:"domain-list",children:"domain-list"})]})]}),e("label",{class:"field dhcp-option-value",children:[e("span",{class:"field-label",children:"Value"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:In(r.type,u.code)?.placeholder??(u.format==="ipv4"?"10.20.0.1, 10.20.0.2":u.format==="ipv6"?"fd00:20::1":u.format==="domain-list"?"lab.test, apps.lab.test":u.format==="hex"?"01:02:ff":"lab.test"),value:u.value,onInput:$=>O(_,"value",$.currentTarget.value)})]})]})]},_)),r.dhcpOptions.length===0?e("p",{class:"field-hint",children:"No extra options. Example: code 15, format string, value lab.test advertises the domain name. DNS and router options override the DNS field and gateway."}):null]}),e("datalist",{id:`dhcp-option-codes-${r.type}`,children:Gs(r.type).map(u=>e("option",{value:u.code,label:`${u.label} (${u.format})`}))})]})})]}):K?e(ge,{children:[e("p",{class:"field-hint",children:["DHCPv4 clients can use ",e("code",{children:ie||"the IPv4 .2 service address"}),"; DHCPv6 clients can use ",e("code",{children:x||"the IPv6 .2 service address"}),". Set that address in the DHCP VNF's DNS field (or configure it directly on clients)."]}),e("div",{class:"form-grid",children:e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:"DNS records"}),e("span",{class:"field-hint",children:"Names are served authoritatively. Use a fully qualified name such as vm1.lab1.test; a leading *. wildcard is supported. Types: A, AAAA, CNAME, TXT, PTR."})]}),e("button",{class:"button ghost",type:"button",onClick:fe,children:"Add record"})]}),e("div",{class:"acl-rule-rows",children:[r.dnsRecords.map((u,_)=>e("div",{class:"acl-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"dnat-rule-title",children:["Record ",_+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>de(_),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Name"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:"vm1.lab1.test",value:u.name,onInput:$=>ee(_,"name",$.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Type"}),e("select",{value:u.type,onChange:$=>ee(_,"type",$.currentTarget.value),children:[e("option",{value:"A",children:"A"}),e("option",{value:"AAAA",children:"AAAA"}),e("option",{value:"CNAME",children:"CNAME"}),e("option",{value:"TXT",children:"TXT"}),e("option",{value:"PTR",children:"PTR"})]})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Value"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,placeholder:u.type==="A"?"10.20.0.50":u.type==="AAAA"?"fd00:20::50":u.type==="CNAME"||u.type==="PTR"?"vm1.lab1.test":"text",value:u.value,onInput:$=>ee(_,"value",$.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"TTL"}),e("input",{type:"number",min:0,max:86400,placeholder:"300",value:u.ttl??300,onInput:$=>ee(_,"ttl",Number($.currentTarget.value)||0)})]})]})]},_)),r.dnsRecords.length===0?e("p",{class:"field-hint",children:"No local records yet. Add one, or use this VNF only as a forwarder."}):null]})]})}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:r.dnsForward,onChange:u=>Q("dnsForward",u.currentTarget.checked)}),e("span",{children:"Forward names without a local record to upstream DNS servers"})]}),e("span",{class:"field-hint",children:"When disabled, unknown names receive NXDOMAIN. Local records always take precedence."})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Upstream DNS servers"}),e("input",{type:"text",disabled:!r.dnsForward,autocomplete:"off",spellcheck:!1,placeholder:"1.1.1.1, 8.8.8.8:53",value:r.dnsForwarders,onInput:u=>Q("dnsForwarders",u.currentTarget.value)}),e("span",{class:"field-hint",children:"Comma-separated IPv4/IPv6 addresses or IP:port. Leave empty to use /etc/resolv.conf on the server."})]})]})]}):B?e(ge,{children:e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Transport"}),e("select",{value:r.transport,onChange:u=>Q("transport",u.currentTarget.value),children:[e("option",{value:"memif",children:"memif + gVisor"}),e("option",{value:"tap",children:"VPP tap + kernel TCP"})]}),e("span",{class:"field-hint",children:"TCP MD5 passwords require the tap transport; memif/gVisor does not support TCP MD5."})]}),e("label",{class:"field checkbox-field",children:e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:r.passive,onChange:u=>Q("passive",u.currentTarget.checked)}),e("span",{children:"Passive mode \u2014 wait for the peer to connect"})]})}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Local AS"}),e("input",{type:"number",required:!0,min:1,value:r.localAS,onInput:u=>Q("localAS",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Router ID"}),e("input",{type:"text",required:!0,placeholder:"10.20.0.1",autocomplete:"off",value:r.routerID,onInput:u=>Q("routerID",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Peer AS"}),e("input",{type:"number",required:!0,min:1,value:r.peerAS,onInput:u=>Q("peerAS",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Peer IPv4/IPv6 address"}),e("input",{type:"text",required:!0,placeholder:"169.254.0.2",autocomplete:"off",value:r.peerIP,onInput:u=>Q("peerIP",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Local IPv4/IPv6 CIDR"}),e("input",{type:"text",required:!0,placeholder:"169.254.0.1/30",autocomplete:"off",value:r.localIP,onInput:u=>Q("localIP",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Peer gateway ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"169.254.0.2 (for routed/multihop peers)",autocomplete:"off",value:r.peerGateway,onInput:u=>Q("peerGateway",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Advertised next hop ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"defaults to local_ip",autocomplete:"off",value:r.nextHop,onInput:u=>Q("nextHop",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Multihop TTL"}),e("input",{type:"number",required:!0,min:1,max:255,value:r.multihopTTL,onInput:u=>Q("multihopTTL",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Target VPP table ID"}),e("input",{type:"number",required:!0,min:0,value:r.targetTableID,onInput:u=>Q("targetTableID",u.currentTarget.value)}),e("span",{class:"field-hint",children:"0 is the default VRF; received IPv4 routes are installed here."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["TCP MD5 password ",e("em",{children:"tap only"})]}),e("input",{type:"password",disabled:r.transport==="memif",placeholder:r.transport==="memif"?"Unavailable on memif":"optional",autocomplete:"new-password",value:r.password,onInput:u=>Q("password",u.currentTarget.value)})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Advertised IPv4 prefixes ",e("em",{children:"optional"})]}),e("textarea",{rows:4,placeholder:`10.10.0.0/16 +10.20.0.0/24`,spellcheck:!1,value:r.advertisePrefixes,onInput:u=>Q("advertisePrefixes",u.currentTarget.value)}),e("span",{class:"field-hint",children:"One IPv4 CIDR per line. These prefixes are advertised to the peer."})]})]})}):e("div",{class:"form-grid",children:[e("label",{class:"field",children:[e("span",{class:"field-label",children:"Outside NIC"}),e("select",{required:!0,value:r.outsideInterface,onChange:u=>Q("outsideInterface",u.currentTarget.value),children:[e("option",{value:"",disabled:!0,children:C?"Loading host interfaces\u2026":"Select a wired physical NIC\u2026"}),w.map(u=>{let _=u.default_route||u.physical===!1||u.wireless,$=Array.isArray(u.addresses)&&u.addresses.length?` \u2014 ${u.addresses.join(", ")}`:"",j=u.default_route?" (default route \u2014 blocked)":u.physical===!1?" (virtual \u2014 not selectable)":u.wireless?" (wireless \u2014 not supported)":"";return e("option",{value:u.name,disabled:_,children:[u.name,$,j]},u.name)}),r.outsideInterface&&!w.some(u=>u.name===r.outsideInterface)?e("option",{value:r.outsideInterface,children:[r.outsideInterface," (not currently present)"]}):null]}),e("span",{class:"field-hint",children:tr(w,r.outsideInterface)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Outside address ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"empty = VPP DHCP client",autocomplete:"off",spellcheck:!1,value:r.outsideAddress,onInput:u=>Q("outsideAddress",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["Outside gateway ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"198.51.100.1",autocomplete:"off",spellcheck:!1,value:r.outsideGateway,onInput:u=>Q("outsideGateway",u.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:["NAT44 pool ",e("em",{children:"optional"})]}),e("input",{type:"text",placeholder:"203.0.113.10-203.0.113.20",autocomplete:"off",spellcheck:!1,value:r.nat44Pool,onInput:u=>Q("nat44Pool",u.currentTarget.value)})]}),e("div",{class:"field field-wide",children:[e("div",{class:"panel-head acl-rules-head",children:[e("div",{children:[e("span",{class:"field-label",children:["DNAT / port forwarding ",e("em",{children:"optional"})]}),e("span",{class:"field-hint",children:"Expose services inside this network on the NAT44 outside address. Saving restarts NAT44."})]}),e("button",{class:"button ghost",type:"button",onClick:E,children:"Add rule"})]}),e("div",{class:"acl-rule-rows",children:r.dnatRules.map((u,_)=>e("div",{class:"acl-rule dnat-rule",children:[e("div",{class:"acl-rule-head",children:[e("span",{class:"dnat-rule-title",children:["Rule ",_+1]}),e("button",{class:"row-action danger",type:"button",onClick:()=>re(_),children:"Remove"})]}),e("div",{class:"acl-rule-fields",children:[e("label",{class:"field dnat-field-protocol",children:[e("span",{class:"field-label",children:"Protocol"}),e("select",{value:u.protocol,onChange:$=>z(_,"protocol",$.currentTarget.value),children:[e("option",{value:"tcp",children:"TCP"}),e("option",{value:"udp",children:"UDP"})]})]}),e("label",{class:"field dnat-field-external-port",children:[e("span",{class:"field-label",children:"External port"}),e("input",{type:"number",min:1,max:65535,step:1,required:!0,inputMode:"numeric",placeholder:"8080",value:u.external_port||"",onInput:$=>z(_,"external_port",Number($.currentTarget.value))})]}),e("label",{class:"field dnat-field-local-ip",children:[e("span",{class:"field-label",children:"Internal address"}),e("input",{type:"text",required:!0,autocomplete:"off",spellcheck:!1,inputMode:"decimal",placeholder:"10.20.0.100",value:u.local_ip,onInput:$=>z(_,"local_ip",$.currentTarget.value)})]}),e("label",{class:"field dnat-field-local-port",children:[e("span",{class:"field-label",children:"Internal port"}),e("input",{type:"number",min:1,max:65535,step:1,required:!0,inputMode:"numeric",placeholder:"80",value:u.local_port||"",onInput:$=>z(_,"local_port",Number($.currentTarget.value))})]})]})]},_))})]})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:r.enabled,disabled:!p&&!l,onChange:u=>Q("enabled",u.currentTarget.checked)}),e("span",{children:"Enable this VNF"})]}),e("span",{class:"field-hint",children:me})]}),m?e("p",{class:"form-error",role:"alert",children:m}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:A,onClick:g,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:A,children:A?"Saving\u2026":p?"Save VNF":"Add VNF"})]})]})})}function or(t){return t.replace(/^SESSION_STATE_/,"").toLowerCase()}function js({vnf:t,onClose:n}){let[a,l]=c(null),[s,o]=c(""),[f,r]=c(!1),b=ne(null),w=async()=>{r(!0);try{let h=await V(`/api/vnfs/${t.id}/bgp`);l(h),o("")}catch(h){o(h instanceof Error?h.message:String(h))}finally{r(!1)}};G(()=>{b.current?.showModal(),w()},[]);let d=a?.session_state||"",C=a?a.established?"established":a.running?or(d)||"starting":"not running":"loading";return e("dialog",{ref:b,class:"vnf-bgp-dialog","aria-labelledby":"vnf-bgp-title",onClose:n,onClick:h=>{h.target===h.currentTarget&&b.current?.close()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"BGP monitor"}),e("h2",{id:"vnf-bgp-title",children:["BGP status \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>b.current?.close(),children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.network_name||"network"," \xB7 local AS ",t.config.local_as||"\u2014"," \xB7 peer AS"," ",t.config.peer_as||"\u2014"]}),s?e("div",{class:"banner",role:"alert",children:s}):null,a?e(ge,{children:[e("div",{class:"bgp-summary",children:[e("span",{class:"status-badge","data-status":a.established?"running":a.running?"degraded":"disabled",children:C}),e("span",{class:"bgp-peer",children:["peer ",a.peer_address||t.config.peer_ip||"\u2014",a.peer_as?` \xB7 AS${a.peer_as}`:""]}),e("span",{class:"bgp-local",children:["local AS",a.local_as||"\u2014"]})]}),e("h3",{class:"bgp-section-title",children:"Advertised prefixes"}),e("div",{class:"table-wrap",children:a.advertised&&a.advertised.length>0?e("table",{children:[e("thead",{children:e("tr",{children:e("th",{scope:"col",children:"Prefix"})})}),e("tbody",{children:a.advertised.map(h=>e("tr",{children:e("td",{class:"mono",children:h})},h))})]}):e("div",{class:"empty",children:e("p",{children:"No advertised prefixes."})})}),e("h3",{class:"bgp-section-title",children:"Received prefixes"}),e("div",{class:"table-wrap",children:a.received&&a.received.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Prefix"}),e("th",{scope:"col",children:"Next hop"}),e("th",{scope:"col",children:"AS path"})]})}),e("tbody",{children:a.received.map(h=>e("tr",{children:[e("td",{class:"mono",children:h.prefix}),e("td",{class:"mono",children:h.next_hop||"\u2014"}),e("td",{class:"mono",children:h.as_path&&h.as_path.length>0?h.as_path.join(" "):"\u2014"})]},h.prefix))})]}):e("div",{class:"empty",children:e("p",{children:"No received prefixes."})})})]}):e("p",{class:"dialog-lede",children:f?"Loading\u2026":"BGP runtime not running."}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:f,onClick:()=>{w()},children:f?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>b.current?.close(),children:"Close"})]})]})}function ir(t,n){return t==="static"?"static":t==="dhcp"?n?`DHCP #${n}`:"DHCP":t||"\u2014"}function Js({vnf:t,onClose:n}){let[a,l]=c(null),[s,o]=c(""),[f,r]=c(!1),b=ne(null),w=async()=>{r(!0);try{let C=await V(`/api/vnfs/${t.id}/dns-records`);l(C),o("")}catch(C){o(C instanceof Error?C.message:String(C))}finally{r(!1)}};G(()=>{b.current?.showModal(),w()},[]);let d=a?.records||[];return e("dialog",{ref:b,class:"vnf-bgp-dialog","aria-labelledby":"vnf-dns-records-title",onClose:n,onClick:C=>{C.target===C.currentTarget&&b.current?.close()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"DNS records"}),e("h2",{id:"vnf-dns-records-title",children:["DNS records \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>b.current?.close(),children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.network_name||"network"," \xB7 ",d.length," record",d.length===1?"":"s"," \xB7 static and DHCP-derived"]}),s?e("div",{class:"banner",role:"alert",children:s}):null,a?.dynamic_error?e("div",{class:"banner",role:"alert",children:["Dynamic records unavailable: ",a.dynamic_error]}):null,e("div",{class:"table-wrap",children:d.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Type"}),e("th",{scope:"col",children:"Value"}),e("th",{scope:"col",children:"TTL"}),e("th",{scope:"col",children:"Source"})]})}),e("tbody",{children:d.map((C,h)=>e("tr",{children:[e("td",{class:"mono",children:C.name}),e("td",{class:"mono",children:C.type}),e("td",{class:"mono",children:C.value}),e("td",{class:"mono",children:C.ttl||"\u2014"}),e("td",{children:ir(C.source,C.source_vnf_id)})]},`${C.name}-${C.type}-${C.value}-${h}`))})]}):e("div",{class:"empty",children:e("p",{children:f?"Loading records\u2026":"No DNS records."})})}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:f,onClick:()=>{w()},children:f?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>b.current?.close(),children:"Close"})]})]})}function Xs({vnf:t,onClose:n}){let[a,l]=c([]),[s,o]=c(""),[f,r]=c(!1),b=ne(null),w=async()=>{r(!0);try{let d=await V(`/api/vnfs/${t.id}/leases`);l(Array.isArray(d.leases)?d.leases:[]),o("")}catch(d){o(d instanceof Error?d.message:String(d))}finally{r(!1)}};return G(()=>{b.current?.showModal(),w()},[]),e("dialog",{ref:b,class:"vnf-leases-dialog","aria-labelledby":"vnf-leases-title",onClose:n,onClick:d=>{d.target===d.currentTarget&&b.current?.close()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Lease monitor"}),e("h2",{id:"vnf-leases-title",children:["Leases \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>b.current?.close(),children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.type==="dhcpv6"?"DHCPv6":"DHCPv4"," \xB7 ",t.network_name||"network"]}),s?e("div",{class:"banner",role:"alert",children:s}):null,e("div",{class:"table-wrap",children:a.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Client ID"}),e("th",{scope:"col",children:"Address"}),e("th",{scope:"col",children:"Hostname"}),e("th",{scope:"col",children:"Expires"}),e("th",{scope:"col",children:"Active"})]})}),e("tbody",{children:a.map(d=>e("tr",{children:[e("td",{class:"mono",children:d.client_id||"\u2014"}),e("td",{class:"mono",children:d.address||"\u2014"}),e("td",{children:d.hostname||"\u2014"}),e("td",{children:Wt(d.expires_at)}),e("td",{children:e("span",{class:"status-badge","data-status":d.active?"running":"disabled",children:d.active?"active":"expired"})})]},`${d.client_id}-${d.address}`))})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25CC"}),e("h3",{children:"No leases yet"}),e("p",{children:"Clients will appear here after they receive an address from this VNF."})]})}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:f,onClick:()=>{w()},children:f?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>b.current?.close(),children:"Close"})]})]})}function Zs({vnf:t,onClose:n}){let[a,l]=c([]),[s,o]=c(""),[f,r]=c(!1),b=ne(null),w=async()=>{r(!0);try{let d=await V(`/api/vnfs/${t.id}/sessions`);l(Array.isArray(d.sessions)?d.sessions:[]),o("")}catch(d){o(d instanceof Error?d.message:String(d))}finally{r(!1)}};return G(()=>{b.current?.showModal(),w()},[]),e("dialog",{ref:b,class:"vnf-sessions-dialog","aria-labelledby":"vnf-sessions-title",onClose:n,onClick:d=>{d.target===d.currentTarget&&b.current?.close()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"NAT monitor"}),e("h2",{id:"vnf-sessions-title",children:["NAT44 sessions \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:()=>b.current?.close(),children:"\xD7"})]}),e("p",{class:"dialog-lede",children:[t.network_name||"network"," \xB7 outside ",t.config.outside_interface||"\u2014"]}),s?e("div",{class:"banner",role:"alert",children:s}):null,e("pre",{class:"nat-sessions",children:a.length>0?a.join(` +`):f?"Loading\u2026":"No active NAT44 sessions."}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:f,onClick:()=>{w()},children:f?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:()=>b.current?.close(),children:"Close"})]})]})}function Ys(){let[t,n]=c([]),[a,l]=c([]),[s,o]=c(!1),[f,r]=c(!0),[b,w]=c(""),[d,C]=c(null),[h,A]=c(null),[I,m]=c(null),[y,P]=c(null),[L,S]=c(null),N=Ne("vnf"),p=ze();G(()=>{if(p.segments[0]!=="vnfs"){C(null),A(null),m(null),P(null),S(null);return}let E=p.segments[1];if(E==="new"){let z=Number(p.query.get("network"))||void 0;A(null),m(null),P(null),S(null),C({vnf:null,networkID:z});return}if(E&&E!=="new"){let z=t.find(ue=>String(ue.id)===E),re=p.segments[2];z&&(C(re==="edit"?{vnf:z}:null),A(re==="leases"?z:null),m(re==="sessions"?z:null),P(re==="bgp"?z:null),S(re==="dns"?z:null));return}C(null),A(null),m(null),P(null),S(null)},[p.path,p.search,t]);let T=E=>{n(E),window.dispatchEvent(new CustomEvent("rookery:vnfs-changed",{detail:E}))},B=async(E=!0)=>{try{let z=await V("/api/vnfs");T(Array.isArray(z)?z:[]),E&&w("")}catch(z){E&&w(z instanceof Error?z.message:String(z))}},K=async()=>{try{let E=await V("/api/networks");l(Array.isArray(E)?E:[])}catch{l([])}},Z=async()=>{try{let E=await V("/api/status");o(!!E.vpp?.ready)}catch{o(!1)}},ie=async()=>{r(!0),await Promise.allSettled([B(),K(),Z()]),r(!1)};G(()=>{ie();let E=k=>{let O=k.detail;Array.isArray(O)?l(O):K(),B(!1)},z=k=>{o(!!k.detail)},re=k=>{let O=k.detail;Array.isArray(O)&&n(O)},ue=k=>{let O=k.detail?.networkId;te(O?`/vnfs/new?network=${O}`:"/vnfs/new")},v=()=>{K()},M=k=>{k.detail!=="vnfs"&&(C(null),A(null),m(null),S(null))};return window.addEventListener("rookery:networks-changed",E),window.addEventListener("rookery:vpp-ready",z),window.addEventListener("rookery:vnfs-data",re),window.addEventListener("rookery:add-vnf",ue),window.addEventListener("rookery:refresh-networks",v),window.addEventListener("rookery:tab-changed",M),()=>{window.removeEventListener("rookery:networks-changed",E),window.removeEventListener("rookery:vpp-ready",z),window.removeEventListener("rookery:vnfs-data",re),window.removeEventListener("rookery:add-vnf",ue),window.removeEventListener("rookery:refresh-networks",v),window.removeEventListener("rookery:tab-changed",M)}},[]),G(()=>{let E=document.getElementById("vnf-tab-count");E&&(E.textContent=String(t.length))},[t.length]);let x=()=>{window.dispatchEvent(new CustomEvent("rookery:refresh-networks"))},R=E=>{C(null),te("/vnfs",!0),w(""),ae(`${d?.vnf?"Saved":"Added"} VNF ${E.name}`),B(!1),(E.type==="nat44"||E.type==="dns")&&x()},F=async E=>{try{await V(`/api/vnfs/${E.id}/restart`,{method:"POST"}),w(""),ae(`Started VNF ${E.name}`),B(!1)}catch(z){let re=z instanceof Error?z.message:String(z);w(`Could not start ${E.name}: ${re}`),z instanceof Xe&&z.status===503&&Z()}},W=async E=>{if(window.confirm(E.type==="nat44"?`Delete VNF "${E.name}"? This stops NAT44 and removes its VPP rules. The network BVI is kept until the network is deleted.`:E.type==="dns"?`Delete VNF "${E.name}"? This stops the DNS server and removes its local records.`:`Delete VNF "${E.name}"? -This stops the function, removes its tap, and deletes its leases.`))try{await V(`/api/vnfs/${E.id}`,{method:"DELETE"});let se=t.filter(ce=>ce.id!==E.id);T(se),w(""),oe(`Deleted VNF ${E.name}`),E.type==="nat44"&&x()}catch(se){w(`Could not delete ${E.name}: ${se instanceof Error?se.message:String(se)}`)}};if(u)return e(Ga,{vnf:u.vnf,networks:s,vnfs:t,vppReady:a,initialNetworkID:u.networkID,onClose:()=>{k(null),Q("/vnfs",!0)},onSaved:R},u.vnf?`edit-${u.vnf.id}`:`new-${u.networkID??"default"}`);let Y=t.filter(E=>E.enabled).length,y=t.reduce((E,M)=>E+(M.lease_count||0),0),ae=t.length===0?f?"Loading VNFs\u2026":"No network functions registered":`${t.length} VNF${t.length===1?"":"s"} \xB7 ${Y} enabled \xB7 ${y} active leases`;return e(ge,{children:[e("section",{class:"panel","aria-labelledby":"vnfs-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"vnfs-heading",children:"Network functions"}),e("p",{class:"panel-subtitle",children:ae})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",onClick:()=>{ne()},children:"Refresh"}),e("button",{class:"button primary",type:"button",disabled:s.length===0,title:s.length===0?"Create a Network first":"",onClick:()=>Q("/vnfs/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add VNF"]})]})]}),b?e("div",{class:"banner",role:"alert",children:b}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Type"}),e("th",{scope:"col",children:"Network"}),e("th",{scope:"col",children:"Status"}),e("th",{scope:"col",children:"Active leases"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(E=>{let M=s.find($=>$.id===E.network_id),se=E.type==="nat44"?"NAT44":E.type==="bgp"?"BGP":E.type==="dns"?"DNS":E.type==="dhcpv6"?"DHCPv6":"DHCPv4",ce=E.type==="dhcpv4"||E.type==="dhcpv6",v=Array.isArray(E.config.dnat_rules)?E.config.dnat_rules.length:0;return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:E.name}),E.type==="nat44"&&E.config.outside_interface?e("span",{class:"vm-description",children:["outside ",E.config.outside_interface,v?` \xB7 ${v} DNAT`:""]}):E.type==="dns"?e("span",{class:"vm-description",children:[Array.isArray(E.config.dns_records)?E.config.dns_records.length:0," static",E.config.dns_forward?" \xB7 forwarding":" \xB7 authoritative"]}):E.tap_host_name?e("span",{class:"vm-description",children:[E.tap_host_name,E.type==="dhcpv4"&&Array.isArray(E.config.static_assignments)&&E.config.static_assignments.length>0?` \xB7 ${E.config.static_assignments.length} static`:""]}):null,e(De,{entityType:"vnf",entityID:E.id,name:E.name,labels:N.labels[String(E.id)],onSaved:$=>N.setEntityLabels(String(E.id),$)})]}),e("td",{class:"mono",children:se}),e("td",{class:"mono",children:M?.name||E.network_name||`network ${E.network_id}`}),e("td",{children:e("span",{class:"status-badge","data-status":E.status||"disabled",title:E.status_detail,children:E.status||"disabled"})}),e("td",{class:ce&&E.lease_count?"mono":"mono muted",children:ce?String(E.lease_count||0):"\u2014"}),e("td",{class:"updated hide-sm",children:Wt(E.updated_at)}),e("td",{children:e("div",{class:"actions",children:[E.type==="bgp"?e("button",{class:"row-action",type:"button",onClick:()=>Q(`/vnfs/${E.id}/bgp`),children:"Status"}):E.type==="dns"?e("button",{class:"row-action",type:"button",onClick:()=>Q(`/vnfs/${E.id}/dns`),children:"Records"}):e("button",{class:"row-action",type:"button",onClick:()=>Q(`/vnfs/${E.id}/${E.type==="nat44"?"sessions":"leases"}`),children:E.type==="nat44"?"Sessions":"Leases"}),e("button",{class:"row-action",type:"button",onClick:()=>Q(`/vnfs/${E.id}/edit`),children:"Edit"}),e("button",{class:"row-action",type:"button",disabled:!a,title:a?"":"VPP must be ready to start a VNF.",onClick:()=>{F(E)},children:E.enabled?"Restart":"Start"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{W(E)},children:"Delete"})]})})]},E.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25C8"}),e("h3",{children:"No network functions yet"}),e("p",{children:"Add a DHCP, DNS, NAT44, or BGP function and attach it to a Network."}),e("button",{class:"button primary",type:"button",disabled:s.length===0,onClick:()=>Q("/vnfs/new"),children:"Add a VNF"})]})})]}),h?e(Xa,{vnf:h,onClose:()=>{A(null),Q("/vnfs",!0)}},`leases-${h.id}`):null,I?e(Za,{vnf:I,onClose:()=>{m(null),Q("/vnfs",!0)}},`sessions-${I.id}`):null,g?e(Ka,{vnf:g,onClose:()=>{P(null),Q("/vnfs",!0)}},`bgp-${g.id}`):null,L?e(Ja,{vnf:L,onClose:()=>{S(null),Q("/vnfs",!0)}},`dns-${L.id}`):null]})}function Ya(t){t&&Fe(e(ja,{}),t)}function Ge(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function zt(t){if(t>=1024){let n=t/1024;return`${Number.isInteger(n)?n:n.toFixed(1)} GB`}return`${t} MB`}function Qa(t){let n=Math.max(0,Math.round(Number(t)||0)),s=Math.floor(n/86400),l=Math.floor(n%86400/3600),a=Math.floor(n%3600/60),o=n%60;return s>0?`${s}d ${l}h`:l>0?`${l}h ${a}m`:a>0?`${a}m ${o}s`:`${o}s`}function ir(t){if(!t||typeof t.getContext!="function")return null;let n=t.getContext("2d");if(!n)return null;let s=window.devicePixelRatio||1,l=Math.max(280,Math.round(t.getBoundingClientRect().width||620)),a=150,o=Math.round(l*s),f=Math.round(a*s);return(t.width!==o||t.height!==f)&&(t.width=o,t.height=f),n.setTransform(s,0,0,s,0,0),{context:n,width:l,height:a}}function wt(t,n,s={}){let l=ir(t);if(!l)return;let{context:a,width:o,height:f}=l;a.clearRect(0,0,o,f);let r={left:52,right:10,top:12,bottom:18},b=Math.max(1,o-r.left-r.right),w=Math.max(1,f-r.top-r.bottom),u=s.format||(m=>String(Math.round(m))),k=n.flatMap(m=>m.values||[]).filter(m=>Number.isFinite(m)),h=s.max||0;h||(h=k.length?Math.max(...k)*1.15:0),h<=0&&(h=1),a.strokeStyle="rgba(148, 163, 184, 0.14)",a.fillStyle="#94a3b8",a.font="10px SFMono-Regular, Consolas, monospace",a.lineWidth=1;for(let m=0;m<=2;m+=1){let g=r.top+w*m/2;a.beginPath(),a.moveTo(r.left,g),a.lineTo(r.left+b,g),a.stroke();let P=u(h*(1-m/2));a.fillText(P,4,g+3)}let A=Number(s.marker);if(Number.isFinite(A)&&A>0&&A<=h){let m=r.top+w-A/h*w;a.save(),a.setLineDash([4,4]),a.strokeStyle="rgba(226, 232, 240, 0.35)",a.beginPath(),a.moveTo(r.left,m),a.lineTo(r.left+b,m),a.stroke(),a.restore()}let I=!1;for(let m of n){let g=m.values||[];if(g.length===0)continue;a.strokeStyle=m.color,a.fillStyle=m.color,a.lineWidth=2,a.lineJoin="round",a.lineCap="round",a.beginPath();let P=!1;for(let L=0;L0?t.disks:t.disk?[t.disk]:t.disk_id!==void 0?[]:[]:[]}function dr(t){return Array.isArray(t?.interfaces)?t.interfaces:[]}function eo({vm:t,disks:n,media:s,interfaces:l,thinPoolReady:a,vppReady:o,onClose:f,onSaved:r}){let b=t!==null,[w,u]=d(t?.name??""),[k,h]=d(t?.description??""),A=t?.status??"stopped",[I,m]=d(String(t?.vcpus??1)),[g,P]=d(String(t?.memory_mb??512)),[L,S]=d(!!t?.console_enabled),[N,p]=d(null),[T,H]=d(t?.cgroup_class||""),[G,J]=ke("vm",t?.id),[ne,x]=d(t?.boot_media_id!==void 0?String(t.boot_media_id):""),[R,F]=d(()=>lr(t)),[W,Y]=d(()=>dr(t)),[y,ae]=d(""),[E,M]=d(""),[se,ce]=d(""),[v,$]=d(""),[C,U]=d(!1),[de,ie]=d(""),pe=te(null),ee=te(null),ue=te(null),me=!!(t&&(t.status==="running"||t.status==="starting")),c=new Set(R.map(O=>O.id)),_=new Set(W.map(O=>O.id)),B=Array.isArray(N?.classes)?N.classes:[],X=B.filter(O=>!B.some(_e=>_e.name.startsWith(O.name+"/"))),Z=n.filter(O=>O.is_template||c.has(O.id)?!1:!O.attached_vm_id||t!==null&&O.attached_vm_id===t.id),fe=l.filter(O=>_.has(O.id)?!1:!O.attached_vm_id&&!O.attached_lightvm_id||t!==null&&O.attached_vm_id===t.id);z(()=>{pe.current?.showModal(),ee.current?.focus()},[]),z(()=>{V("/api/cgroup-settings").then(p).catch(()=>p(null))},[]),z(()=>{me?ce("Stop the VM before changing its disks."):Z.length===0&&R.length===0?ce("Create a disk on the Disks tab first."):ce("Choose a disk and click Add. The first disk is the boot disk. Remove disks with the buttons below.")},[Z.length,R.length,me]),z(()=>{me?$("Stop the VM before changing network interfaces."):fe.length===0&&W.length===0?$("Create a Rookery Virtual Interface on the Network tab first."):$("Choose an interface and click Add. Remove interfaces with the buttons below.")},[fe.length,W.length,me]);let Ie=()=>{C||pe.current?.close()},le=()=>{let O=Number(y);if(!O||me)return;let Ee=n.find(q=>q.id===O)??R.find(q=>q.id===O);Ee&&(F(q=>q.some(we=>we.id===Ee.id)?q:[...q,Ee]),ae(""))},be=O=>{me||F(_e=>_e.filter(Ee=>Ee.id!==O))},ve=()=>{let O=Number(E);if(!O||me)return;let _e=l.find(Ee=>Ee.id===O)||W.find(Ee=>Ee.id===O);_e&&(Y(Ee=>Ee.some(q=>q.id===_e.id)?Ee:[...Ee,_e]),M(""))},K=O=>{me||Y(_e=>_e.filter(Ee=>Ee.id!==O))},$e=async O=>{if(O.preventDefault(),C||!ue.current?.reportValidity())return;if(R.length===0){ie("Select at least one disk.");return}if(!b){let q=R.some(we=>we.source!=="host-device");if(!o){ie("FD.io VPP is not ready. VM creation is blocked until VPP is reachable.");return}if(q&&!a){ie("The thin pool is not ready. Select a host passthrough disk or fix the thin pool first.");return}}let _e={name:w.trim(),description:k.trim(),vcpus:Number(I),memory_mb:Number(g),console_enabled:L};b?me||(_e.disk_ids=R.map(q=>q.id),_e.interface_ids=W.map(q=>q.id),_e.replace_disk_ids=!0,_e.replace_interface_ids=!0):(_e.disk_ids=R.map(q=>q.id),W.length>0&&(_e.interface_ids=W.map(q=>q.id)));let Ee=Number(ne);Ee&&(_e.boot_media_id=Ee),_e.cgroup_class=T,ie(""),U(!0);try{let q=await V(b?`/api/vms/${t?.id}`:"/api/vms",{method:b?"PUT":"POST",body:JSON.stringify(_e)});await Ce("vm",q.id,q.name||w.trim(),G),r(q)}catch(q){ie(q instanceof Error?q.message:String(q))}finally{U(!1)}};return e("dialog",{ref:pe,id:"vm-dialog","aria-labelledby":"dialog-title",onClose:f,onClick:O=>{O.target===O.currentTarget&&Ie()},onCancel:O=>{C&&O.preventDefault()},children:e("form",{ref:ue,novalidate:!0,onSubmit:O=>{$e(O)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:b?"Edit instance":"New instance"}),e("h2",{id:"dialog-title",children:b?`Edit ${t?.name}`:"Create VM"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:Ie,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:ee,type:"text",required:!0,maxlength:64,pattern:rr,autocomplete:"off",spellcheck:!1,placeholder:"web-01",value:w,onInput:O=>u(O.currentTarget.value)}),e("span",{class:"field-hint",children:"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Power"}),e("input",{type:"text",value:b?A:"Starts automatically",disabled:!0}),e("span",{class:"field-hint",children:b?"Managed by Start/Stop on the inventory table.":"New VMs start right away; use Stop on the inventory table to shut one down."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"vCPUs"}),e("input",{type:"number",required:!0,min:1,max:128,step:1,value:I,onInput:O=>m(O.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Memory (MB)"}),e("input",{type:"number",required:!0,min:128,max:1048576,step:128,value:g,onInput:O=>P(O.currentTarget.value)})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Disks"}),e("div",{class:"interface-picker",children:[e("select",{value:y,disabled:me||Z.length===0,onChange:O=>ae(O.currentTarget.value),children:[e("option",{value:"",children:Z.length>0?"Select a disk\u2026":"No disks available"}),Z.map(O=>e("option",{value:String(O.id),children:[O.name," \xB7 ",Nn(O.size_bytes||O.size_gb*1024**3),O.source==="host-device"?" \xB7 passthrough":""]},O.id))]}),e("button",{class:"button ghost",type:"button",disabled:me||Z.length===0,onClick:le,children:"Add"})]}),e("ul",{class:"interface-list",children:R.length===0?e("li",{class:"empty",children:"No disks selected"}):R.map((O,_e)=>e("li",{children:[e("span",{class:"interface-list-name",children:[O.name,e("span",{class:"interface-list-network",children:[Nn(O.size_bytes||O.size_gb*1024**3),O.source==="host-device"?" \xB7 passthrough":"",_e===0?" \xB7 boot":""]})]}),e("button",{class:"row-action danger",type:"button",disabled:me,onClick:()=>be(O.id),children:"Remove"})]},O.id))}),e("span",{class:"field-hint",children:se})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Boot media ",e("em",{children:"optional"})]}),e("select",{value:ne,onChange:O=>x(O.currentTarget.value),children:[e("option",{value:"",children:"Boot from disk"}),s.map(O=>e("option",{value:String(O.id),children:[O.name," \xB7 ",Nn(O.size_bytes)]},O.id))]}),e("span",{class:"field-hint",children:"Read-only ISO from the Installation media tab. You can change this later."})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"field-label",children:"Console access"}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:L,onChange:O=>S(O.currentTarget.checked)}),e("span",{children:"Allow end users to open this VM's console"})]}),e("span",{class:"field-hint",children:"When disabled, the console page and VNC WebSocket are blocked."})]}),e("div",{class:"field field-wide cgroup-vm-section",children:e("label",{class:"field",children:[e("span",{class:"field-label",children:["Cgroup class ",e("em",{children:"optional"})]}),e("select",{value:T,onChange:O=>H(O.currentTarget.value),children:[e("option",{value:"",children:"Shared cgroup (host defaults)"}),!X.some(O=>O.name===T)&&T?e("option",{value:T,children:[T," (not a leaf class)"]}):null,X.map(O=>e("option",{value:O.name,children:O.name},O.name))]}),e("span",{class:"field-hint",children:["Select a class defined in Settings. Changes are applied immediately to a running VM by moving its QEMU process into the class cgroup.",N&&!N.enabled?" Host-wide cgroup limits are disabled in Settings; the class applies when host cgroup limits are enabled.":""]})]})}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Network interfaces ",e("em",{children:"optional"})]}),e("div",{class:"interface-picker",children:[e("select",{value:E,disabled:me||fe.length===0,onChange:O=>M(O.currentTarget.value),children:[e("option",{value:"",children:fe.length>0?"Select an interface\u2026":"No interfaces available"}),fe.map(O=>e("option",{value:String(O.id),children:[O.name," \xB7 ",O.network_name]},O.id))]}),e("button",{class:"button ghost",type:"button",disabled:me||fe.length===0,onClick:ve,children:"Add"})]}),e("ul",{class:"interface-list",children:W.length===0?e("li",{class:"empty",children:"No interfaces selected"}):W.map(O=>e("li",{children:[e("span",{class:"interface-list-name",children:[O.name,e("span",{class:"interface-list-network",children:O.network_name})]}),e("button",{class:"row-action danger",type:"button",disabled:me,onClick:()=>K(O.id),children:"Remove"})]},O.id))}),e("span",{class:"field-hint",children:v})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Description ",e("em",{children:"optional"})]}),e("textarea",{rows:3,maxlength:2e3,placeholder:"What is this instance for?",value:k,onInput:O=>h(O.currentTarget.value)})]}),e("div",{class:"field field-wide",children:e(Se,{labels:G,onChange:J,disabled:C})})]}),de?e("p",{class:"form-error",role:"alert",children:de}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:C,onClick:Ie,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:C,children:C?b?"Saving\u2026":"Creating\u2026":b?"Save changes":"Create VM"})]})]})})}function Nn(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function to({vm:t,onClose:n}){let[s,l]=d(null),[a,o]=d([]),[f,r]=d(""),[b,w]=d(!1),u=te(!1),k=te(null),h=te(null),A=te(null),I=te(null),m=te(null),g=te(null),P=async()=>{if(!u.current){u.current=!0,w(!0);try{let p=await V(`/api/vms/${encodeURIComponent(t.id)}/metrics`),T=Date.now(),H=Number(p.cpu_seconds_total)||0,G=Number(p.disk_read_bytes_total)||0,J=Number(p.disk_write_bytes_total)||0,ne=Number(p.network_rx_bytes_total)||0,x=Number(p.network_tx_bytes_total)||0,R=h.current,F=null,W=null,Y=null,y=null,ae=null;if(R&&T>R.at){let E=(T-R.at)/1e3;H>=R.cpu&&(F=(H-R.cpu)/E*100),G>=R.read&&(W=(G-R.read)/E),J>=R.write&&(Y=(J-R.write)/E),ne>=R.netRead&&(y=(ne-R.netRead)/E),x>=R.netWrite&&(ae=(x-R.netWrite)/E)}p.running?(h.current={at:T,cpu:H,read:G,write:J,netRead:ne,netWrite:x},o(E=>{let M=[...E,{cpu:F,rss:Number(p.memory_rss_bytes)||0,read:W,write:Y,netRead:y,netWrite:ae}];return M.length>60?M.slice(M.length-60):M})):(h.current=null,o([])),l(p),r("")}catch(p){r(`Could not read VM metrics: ${p instanceof Error?p.message:String(p)}`)}finally{u.current=!1,w(!1)}}};z(()=>{k.current?.showModal(),P();let p=window.setInterval(()=>{document.hidden||P()},2e3);return()=>{window.clearInterval(p)}},[]),z(()=>{let p=a.map(y=>y.cpu),T=a.map(y=>y.rss),H=a.map(y=>y.read),G=a.map(y=>y.write),J=a.map(y=>y.netRead),ne=a.map(y=>y.netWrite),x=p.filter(y=>Number.isFinite(y)),R=T.filter(y=>Number.isFinite(y)),F=[...H,...G].filter(y=>Number.isFinite(y)),W=[...J,...ne].filter(y=>Number.isFinite(y)),Y=Math.max(0,Number(t.memory_mb||0)*1024*1024);wt(A.current,[{values:p,color:"#60a5fa"}],{max:Math.max(100,x.length?Math.max(...x)*1.15:0),format:y=>`${Math.round(y)}%`}),wt(I.current,[{values:T,color:"#34d399"}],{max:Math.max(Y,R.length?Math.max(...R)*1.15:0),marker:Y,format:Ge}),wt(m.current,[{values:H,color:"#f59e0b"},{values:G,color:"#f472b6"}],{max:F.length?Math.max(...F)*1.15:0,format:y=>`${Ge(y)}/s`}),wt(g.current,[{values:J,color:"#38bdf8"},{values:ne,color:"#a78bfa"}],{max:W.length?Math.max(...W)*1.15:0,format:y=>`${Ge(y)}/s`})},[a,t.memory_mb]);let L=()=>{k.current?.close()},S=s,N=!!S?.running;return e("dialog",{ref:k,id:"vm-monitor-dialog","aria-labelledby":"vm-monitor-title",onClose:n,onClick:p=>{p.target===p.currentTarget&&L()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Live telemetry"}),e("h2",{id:"vm-monitor-title",children:["VM monitor \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close monitor",onClick:L,children:"\xD7"})]}),e("p",{class:"dialog-lede",children:"QEMU process metrics"}),f?e("div",{class:"banner",role:"alert",children:f}):null,e("div",{class:"monitor-grid",children:[e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Process"}),e("strong",{class:"metric-value","data-status":N?"running":"stopped",children:N?"running":"stopped"}),e("span",{class:"metric-detail",children:["PID ",S?.pid||"\u2014"]})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"CPU"}),e("strong",{class:"metric-value",children:N?a.length<=1?"sampling\u2026":`${(a[a.length-1]?.cpu??0).toFixed(1)}%`:"\u2014"}),e("span",{class:"metric-detail",children:S?`${S.vcpus} vCPU`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Memory"}),e("strong",{class:"metric-value",children:N?Ge(Number(S?.memory_rss_bytes)||0):"\u2014"}),e("span",{class:"metric-detail",children:S?`${zt(S.memory_mb)} configured${N?` \xB7 ${Ge(Number(S.memory_virtual_bytes)||0)} virtual`:""}`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Disk read"}),e("strong",{class:"metric-value",children:N?a.length<=1?"sampling\u2026":`${Ge(a[a.length-1]?.read??0)}/s`:"\u2014"}),e("span",{class:"metric-detail",children:S?`${Ge(Number(S.disk_read_bytes_total)||0)} total`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Disk write"}),e("strong",{class:"metric-value",children:N?a.length<=1?"sampling\u2026":`${Ge(a[a.length-1]?.write??0)}/s`:"\u2014"}),e("span",{class:"metric-detail",children:S?`${Ge(Number(S.disk_write_bytes_total)||0)} total`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Network I/O"}),e("strong",{class:"metric-value",children:N?a.length<=1?"sampling\u2026":`\u2193 ${Ge(a[a.length-1]?.netRead??0)}/s \xB7 \u2191 ${Ge(a[a.length-1]?.netWrite??0)}/s`:"\u2014"}),e("span",{class:"metric-detail",children:S?`RX ${Ge(Number(S.network_rx_bytes_total)||0)} \xB7 TX ${Ge(Number(S.network_tx_bytes_total)||0)}`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Uptime"}),e("strong",{class:"metric-value",children:N&&S?Qa(Number(S.uptime_seconds)||0):"\u2014"}),e("span",{class:"metric-detail",children:"since QEMU started"})]})]}),e("div",{class:"monitor-charts",children:[e("section",{class:"chart-card",children:[e("div",{class:"chart-head",children:[e("span",{children:"CPU history"}),e("span",{class:"chart-note",children:"last 60 samples"})]}),e("canvas",{ref:A,class:"metric-chart",role:"img","aria-label":"CPU usage history"})]}),e("section",{class:"chart-card",children:[e("div",{class:"chart-head",children:[e("span",{children:"Memory RSS"}),e("span",{class:"chart-note",children:"last 60 samples"})]}),e("canvas",{ref:I,class:"metric-chart",role:"img","aria-label":"Memory RSS history"})]}),e("section",{class:"chart-card",children:[e("div",{class:"chart-head",children:[e("span",{children:"Disk I/O"}),e("span",{class:"chart-legend",children:[e("i",{class:"legend-read"}),"read ",e("i",{class:"legend-write"}),"write"]})]}),e("canvas",{ref:m,class:"metric-chart",role:"img","aria-label":"Disk read and write history"})]}),e("section",{class:"chart-card",children:[e("div",{class:"chart-head",children:[e("span",{children:"Network I/O"}),e("span",{class:"chart-legend",children:[e("i",{class:"legend-net-rx"}),"RX ",e("i",{class:"legend-net-tx"}),"TX"]})]}),e("canvas",{ref:g,class:"metric-chart",role:"img","aria-label":"Network receive and transmit history"})]})]}),e("p",{class:"field-hint",children:S&&N?`Sampled ${new Date(S.sampled_at).toLocaleTimeString()}`:"VM is not running."}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:b,onClick:()=>{P()},children:b?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:L,children:"Close"})]})]})}var no=new Intl.NumberFormat;function cr(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function ur(t){return Array.isArray(t.disks)&&t.disks.length>0?t.disks:t.disk?[t.disk]:[]}function so(){let[t,n]=d([]),[s,l]=d([]),[a,o]=d([]),[f,r]=d([]),[b,w]=d(!1),[u,k]=d(!1),[h,A]=d(!0),[I,m]=d(""),[g,P]=d(null),[L,S]=d(null),N=Ne("vm"),p=ze();z(()=>{if(p.segments[0]!=="vms"){P(null),S(null);return}let v=p.segments[1],$=p.segments[2];if(v==="new"){S(null),P({vm:null});return}if(v&&v!=="new"){let C=t.find(U=>U.id===v);if(C&&$==="edit"){S(null),P({vm:C});return}if(C&&$==="monitor"){P(null),S(C);return}}P(null),S(null)},[p.path,t]);let T=v=>{n(v),window.dispatchEvent(new CustomEvent("rookery:vms-changed",{detail:v}))},H=async(v=!0)=>{try{let $=await V("/api/vms");T(Array.isArray($)?$:[]),v&&m("")}catch($){v&&m($ instanceof Error?$.message:String($))}},G=async()=>{try{let v=await V("/api/disks");l(Array.isArray(v)?v:[])}catch{l([])}},J=async()=>{try{let v=await V("/api/media");o(Array.isArray(v)?v:[])}catch{o([])}},ne=async()=>{try{let v=await V("/api/interfaces");r(Array.isArray(v)?v:[])}catch{r([])}},x=async()=>{try{let v=await V("/api/status");w(!!v.thin_pool?.ready),k(!!v.vpp?.ready)}catch{w(!1),k(!1)}},R=async()=>{A(!0),await Promise.allSettled([H(),G(),J(),ne(),x()]),A(!1)};z(()=>{R();let v=ee=>{let ue=ee.detail;Array.isArray(ue)&&l(ue)},$=ee=>{let ue=ee.detail;Array.isArray(ue)&&o(ue)},C=ee=>{let ue=ee.detail;Array.isArray(ue)&&r(ue)},U=ee=>{let ue=ee.detail;ue&&typeof ue.thinPool=="boolean"&&w(ue.thinPool)},de=ee=>{let ue=ee.detail;typeof ue=="boolean"&&k(ue)},ie=ee=>{ee.detail==="vms"&&R()},pe=()=>{Q("/vms/new")};return window.addEventListener("rookery:disks-changed",v),window.addEventListener("rookery:media-changed",$),window.addEventListener("rookery:interfaces-changed",C),window.addEventListener("rookery:readiness-changed",U),window.addEventListener("rookery:vpp-ready",de),window.addEventListener("rookery:tab-changed",ie),window.addEventListener("rookery:add-vm",pe),()=>{window.removeEventListener("rookery:disks-changed",v),window.removeEventListener("rookery:media-changed",$),window.removeEventListener("rookery:interfaces-changed",C),window.removeEventListener("rookery:readiness-changed",U),window.removeEventListener("rookery:vpp-ready",de),window.removeEventListener("rookery:tab-changed",ie),window.removeEventListener("rookery:add-vm",pe)}},[]),z(()=>{let v=`${t.length} VM${t.length===1?"":"s"}`,$=document.getElementById("vm-count");$&&($.textContent=v);let C=document.getElementById("vm-tab-count");C&&(C.textContent=String(t.length))},[t.length]);let F=s.filter(v=>!v.attached_vm_id&&!v.is_template),W=F.some(v=>v.source==="host-device");z(()=>{let v=document.getElementById("new-vm"),$=document.getElementById("new-vm-hint");if(!v||!$)return;let C="";u?F.length===0?C="Create an unattached disk on the Disks tab first.":!b&&!W&&(C="VM creation is blocked until the thin pool is ready or a host passthrough disk is available."):C="VM creation is blocked until FD.io VPP is ready.",v.disabled=C!=="",v.title=C,$.textContent=C,$.hidden=C===""},[F.length,W,b,u]);let Y=()=>{if(!u){oe("VM creation is blocked until FD.io VPP is ready.",!0);return}if(F.length===0){oe("Create an unattached disk on the Disks tab first.",!0);return}if(!b&&!W){oe("The thin pool is not ready and no host passthrough disk is available.",!0);return}P({vm:null}),Q("/vms/new")},y=async v=>{if(window.confirm(`Delete "${v.name}"? +This stops the function, removes its tap, and deletes its leases.`))try{await V(`/api/vnfs/${E.id}`,{method:"DELETE"});let re=t.filter(ue=>ue.id!==E.id);T(re),w(""),ae(`Deleted VNF ${E.name}`),E.type==="nat44"&&x()}catch(re){w(`Could not delete ${E.name}: ${re instanceof Error?re.message:String(re)}`)}};if(d)return e(Ks,{vnf:d.vnf,networks:a,vnfs:t,vppReady:s,initialNetworkID:d.networkID,onClose:()=>{C(null),te("/vnfs",!0)},onSaved:R},d.vnf?`edit-${d.vnf.id}`:`new-${d.networkID??"default"}`);let Q=t.filter(E=>E.enabled).length,g=t.reduce((E,z)=>E+(z.lease_count||0),0),oe=t.length===0?f?"Loading VNFs\u2026":"No network functions registered":`${t.length} VNF${t.length===1?"":"s"} \xB7 ${Q} enabled \xB7 ${g} active leases`;return e(ge,{children:[e("section",{class:"panel","aria-labelledby":"vnfs-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"vnfs-heading",children:"Network functions"}),e("p",{class:"panel-subtitle",children:oe})]}),e("div",{class:"actions",children:[e("button",{class:"button ghost",type:"button",onClick:()=>{ie()},children:"Refresh"}),e("button",{class:"button primary",type:"button",disabled:a.length===0,title:a.length===0?"Create a Network first":"",onClick:()=>te("/vnfs/new"),children:[e("span",{class:"button-plus","aria-hidden":"true",children:"+"}),"Add VNF"]})]})]}),b?e("div",{class:"banner",role:"alert",children:b}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Type"}),e("th",{scope:"col",children:"Network"}),e("th",{scope:"col",children:"Status"}),e("th",{scope:"col",children:"Active leases"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(E=>{let z=a.find(M=>M.id===E.network_id),re=E.type==="nat44"?"NAT44":E.type==="bgp"?"BGP":E.type==="dns"?"DNS":E.type==="dhcpv6"?"DHCPv6":"DHCPv4",ue=E.type==="dhcpv4"||E.type==="dhcpv6",v=Array.isArray(E.config.dnat_rules)?E.config.dnat_rules.length:0;return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:E.name}),E.type==="nat44"&&E.config.outside_interface?e("span",{class:"vm-description",children:["outside ",E.config.outside_interface,v?` \xB7 ${v} DNAT`:""]}):E.type==="dns"?e("span",{class:"vm-description",children:[Array.isArray(E.config.dns_records)?E.config.dns_records.length:0," static",E.config.dns_forward?" \xB7 forwarding":" \xB7 authoritative"]}):E.tap_host_name?e("span",{class:"vm-description",children:[E.tap_host_name,E.type==="dhcpv4"&&Array.isArray(E.config.static_assignments)&&E.config.static_assignments.length>0?` \xB7 ${E.config.static_assignments.length} static`:""]}):null,e(De,{entityType:"vnf",entityID:E.id,name:E.name,labels:N.labels[String(E.id)],onSaved:M=>N.setEntityLabels(String(E.id),M)})]}),e("td",{class:"mono",children:re}),e("td",{class:"mono",children:z?.name||E.network_name||`network ${E.network_id}`}),e("td",{children:e("span",{class:"status-badge","data-status":E.status||"disabled",title:E.status_detail,children:E.status||"disabled"})}),e("td",{class:ue&&E.lease_count?"mono":"mono muted",children:ue?String(E.lease_count||0):"\u2014"}),e("td",{class:"updated hide-sm",children:Wt(E.updated_at)}),e("td",{children:e("div",{class:"actions",children:[E.type==="bgp"?e("button",{class:"row-action",type:"button",onClick:()=>te(`/vnfs/${E.id}/bgp`),children:"Status"}):E.type==="dns"?e("button",{class:"row-action",type:"button",onClick:()=>te(`/vnfs/${E.id}/dns`),children:"Records"}):e("button",{class:"row-action",type:"button",onClick:()=>te(`/vnfs/${E.id}/${E.type==="nat44"?"sessions":"leases"}`),children:E.type==="nat44"?"Sessions":"Leases"}),e("button",{class:"row-action",type:"button",onClick:()=>te(`/vnfs/${E.id}/edit`),children:"Edit"}),e("button",{class:"row-action",type:"button",disabled:!s,title:s?"":"VPP must be ready to start a VNF.",onClick:()=>{F(E)},children:E.enabled?"Restart":"Start"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{W(E)},children:"Delete"})]})})]},E.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u25C8"}),e("h3",{children:"No network functions yet"}),e("p",{children:"Add a DHCP, DNS, NAT44, or BGP function and attach it to a Network."}),e("button",{class:"button primary",type:"button",disabled:a.length===0,onClick:()=>te("/vnfs/new"),children:"Add a VNF"})]})})]}),h?e(Xs,{vnf:h,onClose:()=>{A(null),te("/vnfs",!0)}},`leases-${h.id}`):null,I?e(Zs,{vnf:I,onClose:()=>{m(null),te("/vnfs",!0)}},`sessions-${I.id}`):null,y?e(js,{vnf:y,onClose:()=>{P(null),te("/vnfs",!0)}},`bgp-${y.id}`):null,L?e(Js,{vnf:L,onClose:()=>{S(null),te("/vnfs",!0)}},`dns-${L.id}`):null]})}function Qs(t){t&&Fe(e(Ys,{}),t)}function Ge(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function zt(t){if(t>=1024){let n=t/1024;return`${Number.isInteger(n)?n:n.toFixed(1)} GB`}return`${t} MB`}function eo(t){let n=Math.max(0,Math.round(Number(t)||0)),a=Math.floor(n/86400),l=Math.floor(n%86400/3600),s=Math.floor(n%3600/60),o=n%60;return a>0?`${a}d ${l}h`:l>0?`${l}h ${s}m`:s>0?`${s}m ${o}s`:`${o}s`}function rr(t){if(!t||typeof t.getContext!="function")return null;let n=t.getContext("2d");if(!n)return null;let a=window.devicePixelRatio||1,l=Math.max(280,Math.round(t.getBoundingClientRect().width||620)),s=150,o=Math.round(l*a),f=Math.round(s*a);return(t.width!==o||t.height!==f)&&(t.width=o,t.height=f),n.setTransform(a,0,0,a,0,0),{context:n,width:l,height:s}}function wt(t,n,a={}){let l=rr(t);if(!l)return;let{context:s,width:o,height:f}=l;s.clearRect(0,0,o,f);let r={left:52,right:10,top:12,bottom:18},b=Math.max(1,o-r.left-r.right),w=Math.max(1,f-r.top-r.bottom),d=a.format||(m=>String(Math.round(m))),C=n.flatMap(m=>m.values||[]).filter(m=>Number.isFinite(m)),h=a.max||0;h||(h=C.length?Math.max(...C)*1.15:0),h<=0&&(h=1),s.strokeStyle="rgba(148, 163, 184, 0.14)",s.fillStyle="#94a3b8",s.font="10px SFMono-Regular, Consolas, monospace",s.lineWidth=1;for(let m=0;m<=2;m+=1){let y=r.top+w*m/2;s.beginPath(),s.moveTo(r.left,y),s.lineTo(r.left+b,y),s.stroke();let P=d(h*(1-m/2));s.fillText(P,4,y+3)}let A=Number(a.marker);if(Number.isFinite(A)&&A>0&&A<=h){let m=r.top+w-A/h*w;s.save(),s.setLineDash([4,4]),s.strokeStyle="rgba(226, 232, 240, 0.35)",s.beginPath(),s.moveTo(r.left,m),s.lineTo(r.left+b,m),s.stroke(),s.restore()}let I=!1;for(let m of n){let y=m.values||[];if(y.length===0)continue;s.strokeStyle=m.color,s.fillStyle=m.color,s.lineWidth=2,s.lineJoin="round",s.lineCap="round",s.beginPath();let P=!1;for(let L=0;L0?t.disks:t.disk?[t.disk]:t.disk_id!==void 0?[]:[]:[]}function dr(t){return Array.isArray(t?.interfaces)?t.interfaces:[]}function to({vm:t,disks:n,media:a,interfaces:l,thinPoolReady:s,vppReady:o,onClose:f,onSaved:r}){let b=t!==null,[w,d]=c(t?.name??""),[C,h]=c(t?.description??""),A=t?.status??"stopped",[I,m]=c(String(t?.vcpus??1)),[y,P]=c(String(t?.memory_mb??512)),[L,S]=c(!!t?.console_enabled),[N,p]=c(null),[T,B]=c(t?.cgroup_class||""),[K,Z]=ke("vm",t?.id),[ie,x]=c(t?.boot_media_id!==void 0?String(t.boot_media_id):""),[R,F]=c(()=>cr(t)),[W,Q]=c(()=>dr(t)),[g,oe]=c(""),[E,z]=c(""),[re,ue]=c(""),[v,M]=c(""),[k,O]=c(!1),[le,se]=c(""),fe=ne(null),ee=ne(null),de=ne(null),me=!!(t&&(t.status==="running"||t.status==="starting")),u=new Set(R.map(H=>H.id)),_=new Set(W.map(H=>H.id)),$=Array.isArray(N?.classes)?N.classes:[],j=$.filter(H=>!$.some(_e=>_e.name.startsWith(H.name+"/"))),J=n.filter(H=>H.is_template||u.has(H.id)||H.deletion_state?!1:!H.attached_vm_id||t!==null&&H.attached_vm_id===t.id),pe=l.filter(H=>_.has(H.id)?!1:!H.attached_vm_id&&!H.attached_lightvm_id||t!==null&&H.attached_vm_id===t.id);G(()=>{fe.current?.showModal(),ee.current?.focus()},[]),G(()=>{V("/api/cgroup-settings").then(p).catch(()=>p(null))},[]),G(()=>{me?ue("Stop the VM before changing its disks."):J.length===0&&R.length===0?ue("Create a disk on the Disks tab first."):ue("Choose a disk and click Add. The first disk is the boot disk. Remove disks with the buttons below.")},[J.length,R.length,me]),G(()=>{me?M("Stop the VM before changing network interfaces."):pe.length===0&&W.length===0?M("Create a Rookery Virtual Interface on the Network tab first."):M("Choose an interface and click Add. Remove interfaces with the buttons below.")},[pe.length,W.length,me]);let Le=()=>{k||fe.current?.close()},q=()=>{let H=Number(g);if(!H||me)return;let Ee=n.find(U=>U.id===H)??R.find(U=>U.id===H);Ee&&(F(U=>U.some(we=>we.id===Ee.id)?U:[...U,Ee]),oe(""))},he=H=>{me||F(_e=>_e.filter(Ee=>Ee.id!==H))},ve=()=>{let H=Number(E);if(!H||me)return;let _e=l.find(Ee=>Ee.id===H)||W.find(Ee=>Ee.id===H);_e&&(Q(Ee=>Ee.some(U=>U.id===_e.id)?Ee:[...Ee,_e]),z(""))},X=H=>{me||Q(_e=>_e.filter(Ee=>Ee.id!==H))},$e=async H=>{if(H.preventDefault(),k||!de.current?.reportValidity())return;if(R.length===0){se("Select at least one disk.");return}if(!b){let U=R.some(we=>Gt(we));if(!o){se("FD.io VPP is not ready. VM creation is blocked until VPP is reachable.");return}if(U&&!s){se("The thin pool is not ready. Select a non-LVM disk or fix the thin pool first.");return}}let _e={name:w.trim(),description:C.trim(),vcpus:Number(I),memory_mb:Number(y),console_enabled:L};b?me||(_e.disk_ids=R.map(U=>U.id),_e.interface_ids=W.map(U=>U.id),_e.replace_disk_ids=!0,_e.replace_interface_ids=!0):(_e.disk_ids=R.map(U=>U.id),W.length>0&&(_e.interface_ids=W.map(U=>U.id)));let Ee=Number(ie);Ee&&(_e.boot_media_id=Ee),_e.cgroup_class=T,se(""),O(!0);try{let U=await V(b?`/api/vms/${t?.id}`:"/api/vms",{method:b?"PUT":"POST",body:JSON.stringify(_e)});await Ce("vm",U.id,U.name||w.trim(),K),r(U)}catch(U){se(U instanceof Error?U.message:String(U))}finally{O(!1)}};return e("dialog",{ref:fe,id:"vm-dialog","aria-labelledby":"dialog-title",onClose:f,onClick:H=>{H.target===H.currentTarget&&Le()},onCancel:H=>{k&&H.preventDefault()},children:e("form",{ref:de,novalidate:!0,onSubmit:H=>{$e(H)},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:b?"Edit instance":"New instance"}),e("h2",{id:"dialog-title",children:b?`Edit ${t?.name}`:"Create VM"})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close dialog",onClick:Le,children:"\xD7"})]}),e("div",{class:"form-grid",children:[e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Name"}),e("input",{ref:ee,type:"text",required:!0,maxlength:64,pattern:lr,autocomplete:"off",spellcheck:!1,placeholder:"web-01",value:w,onInput:H=>d(H.currentTarget.value)}),e("span",{class:"field-hint",children:"Letters, digits, dots, dashes and underscores."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Power"}),e("input",{type:"text",value:b?A:"Starts automatically",disabled:!0}),e("span",{class:"field-hint",children:b?"Managed by Start/Stop on the inventory table.":"New VMs start right away; use Stop on the inventory table to shut one down."})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"vCPUs"}),e("input",{type:"number",required:!0,min:1,max:128,step:1,value:I,onInput:H=>m(H.currentTarget.value)})]}),e("label",{class:"field",children:[e("span",{class:"field-label",children:"Memory (MB)"}),e("input",{type:"number",required:!0,min:128,max:1048576,step:128,value:y,onInput:H=>P(H.currentTarget.value)})]}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:"Disks"}),e("div",{class:"interface-picker",children:[e("select",{value:g,disabled:me||J.length===0,onChange:H=>oe(H.currentTarget.value),children:[e("option",{value:"",children:J.length>0?"Select a disk\u2026":"No disks available"}),J.map(H=>e("option",{value:String(H.id),children:[H.name," \xB7 ",Dn(H.size_bytes||H.size_gb*1024**3),H.source==="host-device"?" \xB7 passthrough":H.source==="kouja"?" \xB7 kouja":""]},H.id))]}),e("button",{class:"button ghost",type:"button",disabled:me||J.length===0,onClick:q,children:"Add"})]}),e("ul",{class:"interface-list",children:R.length===0?e("li",{class:"empty",children:"No disks selected"}):R.map((H,_e)=>e("li",{children:[e("span",{class:"interface-list-name",children:[H.name,e("span",{class:"interface-list-network",children:[Dn(H.size_bytes||H.size_gb*1024**3),H.source==="host-device"?" \xB7 passthrough":H.source==="kouja"?" \xB7 kouja":"",_e===0?" \xB7 boot":""]})]}),e("button",{class:"row-action danger",type:"button",disabled:me,onClick:()=>he(H.id),children:"Remove"})]},H.id))}),e("span",{class:"field-hint",children:re})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Boot media ",e("em",{children:"optional"})]}),e("select",{value:ie,onChange:H=>x(H.currentTarget.value),children:[e("option",{value:"",children:"Boot from disk"}),a.map(H=>e("option",{value:String(H.id),children:[H.name," \xB7 ",Dn(H.size_bytes)]},H.id))]}),e("span",{class:"field-hint",children:"Read-only ISO from the Installation media tab. You can change this later."})]}),e("label",{class:"field field-wide checkbox-field",children:[e("span",{class:"field-label",children:"Console access"}),e("span",{class:"checkbox-row",children:[e("input",{type:"checkbox",checked:L,onChange:H=>S(H.currentTarget.checked)}),e("span",{children:"Allow end users to open this VM's console"})]}),e("span",{class:"field-hint",children:"When disabled, the console page and VNC WebSocket are blocked."})]}),e("div",{class:"field field-wide cgroup-vm-section",children:e("label",{class:"field",children:[e("span",{class:"field-label",children:["Cgroup class ",e("em",{children:"optional"})]}),e("select",{value:T,onChange:H=>B(H.currentTarget.value),children:[e("option",{value:"",children:"Shared cgroup (host defaults)"}),!j.some(H=>H.name===T)&&T?e("option",{value:T,children:[T," (not a leaf class)"]}):null,j.map(H=>e("option",{value:H.name,children:H.name},H.name))]}),e("span",{class:"field-hint",children:["Select a class defined in Settings. Changes are applied immediately to a running VM by moving its QEMU process into the class cgroup.",N&&!N.enabled?" Host-wide cgroup limits are disabled in Settings; the class applies when host cgroup limits are enabled.":""]})]})}),e("div",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Network interfaces ",e("em",{children:"optional"})]}),e("div",{class:"interface-picker",children:[e("select",{value:E,disabled:me||pe.length===0,onChange:H=>z(H.currentTarget.value),children:[e("option",{value:"",children:pe.length>0?"Select an interface\u2026":"No interfaces available"}),pe.map(H=>e("option",{value:String(H.id),children:[H.name," \xB7 ",H.network_name]},H.id))]}),e("button",{class:"button ghost",type:"button",disabled:me||pe.length===0,onClick:ve,children:"Add"})]}),e("ul",{class:"interface-list",children:W.length===0?e("li",{class:"empty",children:"No interfaces selected"}):W.map(H=>e("li",{children:[e("span",{class:"interface-list-name",children:[H.name,e("span",{class:"interface-list-network",children:H.network_name})]}),e("button",{class:"row-action danger",type:"button",disabled:me,onClick:()=>X(H.id),children:"Remove"})]},H.id))}),e("span",{class:"field-hint",children:v})]}),e("label",{class:"field field-wide",children:[e("span",{class:"field-label",children:["Description ",e("em",{children:"optional"})]}),e("textarea",{rows:3,maxlength:2e3,placeholder:"What is this instance for?",value:C,onInput:H=>h(H.currentTarget.value)})]}),e("div",{class:"field field-wide",children:e(Se,{labels:K,onChange:Z,disabled:k})})]}),le?e("p",{class:"form-error",role:"alert",children:le}):null,e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:k,onClick:Le,children:"Cancel"}),e("button",{class:"button primary",type:"submit",disabled:k,children:k?b?"Saving\u2026":"Creating\u2026":b?"Save changes":"Create VM"})]})]})})}function Dn(t){let n=Number(t)||0;return n>=1024**3?`${(n/1024**3).toFixed(n%1024**3===0?0:1)} GB`:n>=1024**2?`${(n/1024**2).toFixed(n%1024**2===0?0:1)} MB`:n>=1024?`${(n/1024).toFixed(n%1024===0?0:1)} KB`:`${n} B`}function no({vm:t,onClose:n}){let[a,l]=c(null),[s,o]=c([]),[f,r]=c(""),[b,w]=c(!1),d=ne(!1),C=ne(null),h=ne(null),A=ne(null),I=ne(null),m=ne(null),y=ne(null),P=async()=>{if(!d.current){d.current=!0,w(!0);try{let p=await V(`/api/vms/${encodeURIComponent(t.id)}/metrics`),T=Date.now(),B=Number(p.cpu_seconds_total)||0,K=Number(p.disk_read_bytes_total)||0,Z=Number(p.disk_write_bytes_total)||0,ie=Number(p.network_rx_bytes_total)||0,x=Number(p.network_tx_bytes_total)||0,R=h.current,F=null,W=null,Q=null,g=null,oe=null;if(R&&T>R.at){let E=(T-R.at)/1e3;B>=R.cpu&&(F=(B-R.cpu)/E*100),K>=R.read&&(W=(K-R.read)/E),Z>=R.write&&(Q=(Z-R.write)/E),ie>=R.netRead&&(g=(ie-R.netRead)/E),x>=R.netWrite&&(oe=(x-R.netWrite)/E)}p.running?(h.current={at:T,cpu:B,read:K,write:Z,netRead:ie,netWrite:x},o(E=>{let z=[...E,{cpu:F,rss:Number(p.memory_rss_bytes)||0,read:W,write:Q,netRead:g,netWrite:oe}];return z.length>60?z.slice(z.length-60):z})):(h.current=null,o([])),l(p),r("")}catch(p){r(`Could not read VM metrics: ${p instanceof Error?p.message:String(p)}`)}finally{d.current=!1,w(!1)}}};G(()=>{C.current?.showModal(),P();let p=window.setInterval(()=>{document.hidden||P()},2e3);return()=>{window.clearInterval(p)}},[]),G(()=>{let p=s.map(g=>g.cpu),T=s.map(g=>g.rss),B=s.map(g=>g.read),K=s.map(g=>g.write),Z=s.map(g=>g.netRead),ie=s.map(g=>g.netWrite),x=p.filter(g=>Number.isFinite(g)),R=T.filter(g=>Number.isFinite(g)),F=[...B,...K].filter(g=>Number.isFinite(g)),W=[...Z,...ie].filter(g=>Number.isFinite(g)),Q=Math.max(0,Number(t.memory_mb||0)*1024*1024);wt(A.current,[{values:p,color:"#60a5fa"}],{max:Math.max(100,x.length?Math.max(...x)*1.15:0),format:g=>`${Math.round(g)}%`}),wt(I.current,[{values:T,color:"#34d399"}],{max:Math.max(Q,R.length?Math.max(...R)*1.15:0),marker:Q,format:Ge}),wt(m.current,[{values:B,color:"#f59e0b"},{values:K,color:"#f472b6"}],{max:F.length?Math.max(...F)*1.15:0,format:g=>`${Ge(g)}/s`}),wt(y.current,[{values:Z,color:"#38bdf8"},{values:ie,color:"#a78bfa"}],{max:W.length?Math.max(...W)*1.15:0,format:g=>`${Ge(g)}/s`})},[s,t.memory_mb]);let L=()=>{C.current?.close()},S=a,N=!!S?.running;return e("dialog",{ref:C,id:"vm-monitor-dialog","aria-labelledby":"vm-monitor-title",onClose:n,onClick:p=>{p.target===p.currentTarget&&L()},children:[e("header",{class:"dialog-head",children:[e("div",{children:[e("p",{class:"eyebrow",children:"Live telemetry"}),e("h2",{id:"vm-monitor-title",children:["VM monitor \xB7 ",t.name]})]}),e("button",{class:"icon-button",type:"button","aria-label":"Close monitor",onClick:L,children:"\xD7"})]}),e("p",{class:"dialog-lede",children:"QEMU process metrics"}),f?e("div",{class:"banner",role:"alert",children:f}):null,e("div",{class:"monitor-grid",children:[e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Process"}),e("strong",{class:"metric-value","data-status":N?"running":"stopped",children:N?"running":"stopped"}),e("span",{class:"metric-detail",children:["PID ",S?.pid||"\u2014"]})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"CPU"}),e("strong",{class:"metric-value",children:N?s.length<=1?"sampling\u2026":`${(s[s.length-1]?.cpu??0).toFixed(1)}%`:"\u2014"}),e("span",{class:"metric-detail",children:S?`${S.vcpus} vCPU`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Memory"}),e("strong",{class:"metric-value",children:N?Ge(Number(S?.memory_rss_bytes)||0):"\u2014"}),e("span",{class:"metric-detail",children:S?`${zt(S.memory_mb)} configured${N?` \xB7 ${Ge(Number(S.memory_virtual_bytes)||0)} virtual`:""}`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Disk read"}),e("strong",{class:"metric-value",children:N?s.length<=1?"sampling\u2026":`${Ge(s[s.length-1]?.read??0)}/s`:"\u2014"}),e("span",{class:"metric-detail",children:S?`${Ge(Number(S.disk_read_bytes_total)||0)} total`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Disk write"}),e("strong",{class:"metric-value",children:N?s.length<=1?"sampling\u2026":`${Ge(s[s.length-1]?.write??0)}/s`:"\u2014"}),e("span",{class:"metric-detail",children:S?`${Ge(Number(S.disk_write_bytes_total)||0)} total`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Network I/O"}),e("strong",{class:"metric-value",children:N?s.length<=1?"sampling\u2026":`\u2193 ${Ge(s[s.length-1]?.netRead??0)}/s \xB7 \u2191 ${Ge(s[s.length-1]?.netWrite??0)}/s`:"\u2014"}),e("span",{class:"metric-detail",children:S?`RX ${Ge(Number(S.network_rx_bytes_total)||0)} \xB7 TX ${Ge(Number(S.network_tx_bytes_total)||0)}`:"\u2014"})]}),e("section",{class:"monitor-card",children:[e("span",{class:"metric-label",children:"Uptime"}),e("strong",{class:"metric-value",children:N&&S?eo(Number(S.uptime_seconds)||0):"\u2014"}),e("span",{class:"metric-detail",children:"since QEMU started"})]})]}),e("div",{class:"monitor-charts",children:[e("section",{class:"chart-card",children:[e("div",{class:"chart-head",children:[e("span",{children:"CPU history"}),e("span",{class:"chart-note",children:"last 60 samples"})]}),e("canvas",{ref:A,class:"metric-chart",role:"img","aria-label":"CPU usage history"})]}),e("section",{class:"chart-card",children:[e("div",{class:"chart-head",children:[e("span",{children:"Memory RSS"}),e("span",{class:"chart-note",children:"last 60 samples"})]}),e("canvas",{ref:I,class:"metric-chart",role:"img","aria-label":"Memory RSS history"})]}),e("section",{class:"chart-card",children:[e("div",{class:"chart-head",children:[e("span",{children:"Disk I/O"}),e("span",{class:"chart-legend",children:[e("i",{class:"legend-read"}),"read ",e("i",{class:"legend-write"}),"write"]})]}),e("canvas",{ref:m,class:"metric-chart",role:"img","aria-label":"Disk read and write history"})]}),e("section",{class:"chart-card",children:[e("div",{class:"chart-head",children:[e("span",{children:"Network I/O"}),e("span",{class:"chart-legend",children:[e("i",{class:"legend-net-rx"}),"RX ",e("i",{class:"legend-net-tx"}),"TX"]})]}),e("canvas",{ref:y,class:"metric-chart",role:"img","aria-label":"Network receive and transmit history"})]})]}),e("p",{class:"field-hint",children:S&&N?`Sampled ${new Date(S.sampled_at).toLocaleTimeString()}`:"VM is not running."}),e("footer",{class:"dialog-actions",children:[e("button",{class:"button ghost",type:"button",disabled:b,onClick:()=>{P()},children:b?"Refreshing\u2026":"Refresh"}),e("button",{class:"button primary",type:"button",onClick:L,children:"Close"})]})]})}var ao=new Intl.NumberFormat;function ur(t){if(!t)return"\u2014";let n=new Date(t);return Number.isNaN(n.getTime())?"\u2014":n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function pr(t){return Array.isArray(t.disks)&&t.disks.length>0?t.disks:t.disk?[t.disk]:[]}function so(){let[t,n]=c([]),[a,l]=c([]),[s,o]=c([]),[f,r]=c([]),[b,w]=c(!1),[d,C]=c(!1),[h,A]=c(!0),[I,m]=c(""),[y,P]=c(null),[L,S]=c(null),N=Ne("vm"),p=ze();G(()=>{if(p.segments[0]!=="vms"){P(null),S(null);return}let v=p.segments[1],M=p.segments[2];if(v==="new"){S(null),P({vm:null});return}if(v&&v!=="new"){let k=t.find(O=>O.id===v);if(k&&M==="edit"){S(null),P({vm:k});return}if(k&&M==="monitor"){P(null),S(k);return}}P(null),S(null)},[p.path,t]);let T=v=>{n(v),window.dispatchEvent(new CustomEvent("rookery:vms-changed",{detail:v}))},B=async(v=!0)=>{try{let M=await V("/api/vms");T(Array.isArray(M)?M:[]),v&&m("")}catch(M){v&&m(M instanceof Error?M.message:String(M))}},K=async()=>{try{let v=await V("/api/disks");l(Array.isArray(v)?v:[])}catch{l([])}},Z=async()=>{try{let v=await V("/api/media");o(Array.isArray(v)?v:[])}catch{o([])}},ie=async()=>{try{let v=await V("/api/interfaces");r(Array.isArray(v)?v:[])}catch{r([])}},x=async()=>{try{let v=await V("/api/status");w(!!v.thin_pool?.ready),C(!!v.vpp?.ready)}catch{w(!1),C(!1)}},R=async()=>{A(!0),await Promise.allSettled([B(),K(),Z(),ie(),x()]),A(!1)};G(()=>{R();let v=ee=>{let de=ee.detail;Array.isArray(de)&&l(de)},M=ee=>{let de=ee.detail;Array.isArray(de)&&o(de)},k=ee=>{let de=ee.detail;Array.isArray(de)&&r(de)},O=ee=>{let de=ee.detail;de&&typeof de.thinPool=="boolean"&&w(de.thinPool)},le=ee=>{let de=ee.detail;typeof de=="boolean"&&C(de)},se=ee=>{ee.detail==="vms"&&R()},fe=()=>{te("/vms/new")};return window.addEventListener("rookery:disks-changed",v),window.addEventListener("rookery:media-changed",M),window.addEventListener("rookery:interfaces-changed",k),window.addEventListener("rookery:readiness-changed",O),window.addEventListener("rookery:vpp-ready",le),window.addEventListener("rookery:tab-changed",se),window.addEventListener("rookery:add-vm",fe),()=>{window.removeEventListener("rookery:disks-changed",v),window.removeEventListener("rookery:media-changed",M),window.removeEventListener("rookery:interfaces-changed",k),window.removeEventListener("rookery:readiness-changed",O),window.removeEventListener("rookery:vpp-ready",le),window.removeEventListener("rookery:tab-changed",se),window.removeEventListener("rookery:add-vm",fe)}},[]),G(()=>{let v=`${t.length} VM${t.length===1?"":"s"}`,M=document.getElementById("vm-count");M&&(M.textContent=v);let k=document.getElementById("vm-tab-count");k&&(k.textContent=String(t.length))},[t.length]);let F=a.filter(v=>!v.attached_vm_id&&!v.is_template&&!v.deletion_state),W=F.some(v=>!Gt(v));G(()=>{let v=document.getElementById("new-vm"),M=document.getElementById("new-vm-hint");if(!v||!M)return;let k="";d?F.length===0?k="Create an unattached disk on the Disks tab first.":!b&&!W&&(k="VM creation is blocked until the thin pool is ready or a non-LVM disk is available."):k="VM creation is blocked until FD.io VPP is ready.",v.disabled=k!=="",v.title=k,M.textContent=k,M.hidden=k===""},[F.length,W,b,d]);let Q=()=>{if(!d){ae("VM creation is blocked until FD.io VPP is ready.",!0);return}if(F.length===0){ae("Create an unattached disk on the Disks tab first.",!0);return}if(!b&&!W){ae("The thin pool is not ready and no non-LVM disk is available.",!0);return}P({vm:null}),te("/vms/new")},g=async v=>{if(window.confirm(`Delete "${v.name}"? -This removes the inventory record only. The disk remains available.`))try{await V(`/api/vms/${v.id}`,{method:"DELETE"}),m(""),oe(`Deleted ${v.name}`),await Promise.allSettled([H(),G(),ne()])}catch($){m(`Could not delete ${v.name}: ${$ instanceof Error?$.message:String($)}`)}},ae=async v=>{try{let $=await V(`/api/vms/${v.id}/start`,{method:"POST"});T(t.map(C=>C.id===$.id?$:C)),m(""),oe(`Started ${v.name}`),G()}catch($){m(`Could not start ${v.name}: ${$ instanceof Error?$.message:String($)}`),H()}},E=async v=>{try{let $=await V(`/api/vms/${v.id}/stop`,{method:"POST"});T(t.map(C=>C.id===$.id?$:C)),m(""),oe(`Stopped ${v.name}`),G()}catch($){m(`Could not stop ${v.name}: ${$ instanceof Error?$.message:String($)}`),H()}},M=v=>{if(!v.console_enabled){oe("Console access is disabled. Edit the VM to enable it.",!0);return}if(v.status!=="running"){oe("Start the VM before opening its console.",!0);return}window.open(`/console.html?vm=${encodeURIComponent(v.id)}`,"_blank","noopener")},se=t.filter(v=>v.status==="running").length,ce=t.length===0?h?"Loading instances\u2026":"No instances registered":`${t.length} instance${t.length===1?"":"s"} \xB7 ${se} running`;return e(ge,{children:[e("section",{class:"panel","aria-labelledby":"inventory-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"inventory-heading",children:"Inventory"}),e("p",{class:"panel-subtitle",children:ce})]}),e("button",{class:"button ghost",type:"button",disabled:h,onClick:()=>{R()},children:h?"Refreshing\u2026":"Refresh"})]}),I?e("div",{class:"banner",role:"alert",children:I}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Status"}),e("th",{scope:"col",children:"vCPU"}),e("th",{scope:"col",children:"Memory"}),e("th",{scope:"col",children:"Cgroup"}),e("th",{scope:"col",children:"Disks"}),e("th",{scope:"col",children:"Boot"}),e("th",{scope:"col",children:"Console"}),e("th",{scope:"col",children:"Network"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(v=>{let $=ur(v),C=a.find(ie=>ie.id===v.boot_media_id)||v.boot_media,U=Array.isArray(v.interfaces)?v.interfaces:[],de=v.status==="running"||v.status==="starting";return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:v.name}),v.description?e("span",{class:"vm-description",title:v.description,children:v.description}):null,e(De,{entityType:"vm",entityID:v.id,name:v.name,labels:N.labels[v.id],onSaved:ie=>N.setEntityLabels(v.id,ie)})]}),e("td",{children:e("span",{class:"status-badge","data-status":v.status,children:v.status})}),e("td",{class:"mono",children:no.format(v.vcpus)}),e("td",{class:"mono",children:zt(v.memory_mb)}),e("td",{class:v.cgroup_class?"mono":"mono muted",title:v.cgroup_class?`Cgroup class: ${v.cgroup_class}`:"Shared cgroup",children:v.cgroup_class||"shared"}),e("td",{class:$.length?"mono":"mono muted",title:$.length?$.map(ie=>`${ie.name} \xB7 ${no.format(ie.size_gb)} GB`).join(", "):void 0,children:$.length?$.map(ie=>ie.name).join(", "):"\u2014"}),e("td",{class:C?"mono":"mono muted",title:C?.path||void 0,children:C?C.name:"disk"}),e("td",{class:v.console_enabled?"mono":"mono muted",children:v.console_enabled?"enabled":"disabled"}),e("td",{class:U.length?"mono":"mono muted",title:U.length?U.map(ie=>`${ie.name} (${ie.network_name})`).join(", "):void 0,children:U.length?U.map(ie=>ie.network_name||ie.name).join(", "):"none"}),e("td",{class:"updated hide-sm",children:cr(v.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:de?"row-action danger":"row-action",type:"button",onClick:()=>de?void E(v):void ae(v),children:de?"Stop":"Start"}),e("button",{class:"row-action",type:"button",disabled:!v.console_enabled||v.status!=="running",title:v.console_enabled?v.status!=="running"?"Start the VM to open its console.":"":"Console access is disabled. Edit the VM to enable it.",onClick:()=>M(v),children:"Console"}),e("button",{class:"row-action",type:"button",onClick:()=>Q(`/vms/${v.id}/monitor`),children:"Monitor"}),e("button",{class:"row-action",type:"button",onClick:()=>Q(`/vms/${v.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{y(v)},children:"Delete"})]})})]},v.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2B21"}),e("h3",{children:"No virtual machines yet"}),e("p",{children:"Create the first instance to start building out this host."}),e("button",{class:"button primary",type:"button",disabled:F.length===0,title:F.length===0?"Create an unattached disk on the Disks tab first.":"",onClick:Y,children:"Create a VM"})]})})]}),g?e(eo,{vm:g.vm,disks:s,media:a,interfaces:f,thinPoolReady:b,vppReady:u,onClose:()=>{P(null),Q("/vms",!0)},onSaved:v=>{P(null),Q("/vms",!0),m(""),oe(`${g.vm?"Saved":"Created"} ${v.name}`),Promise.allSettled([H(),G(),ne()])}},g.vm?`edit-${g.vm.id}`:"new"):null,L?e(to,{vm:L,onClose:()=>{S(null),Q("/vms",!0)}},`monitor-${L.id}`):null]})}function ao(t){t&&Fe(e(so,{}),t)}var oo=document.getElementById("toast-root");oo&&Cs(oo);ao(document.getElementById("vms-root"));Us(document.getElementById("disks-root"));ta(document.getElementById("media-root"));Ys(document.getElementById("lightvm-root"));Na(document.getElementById("networks-root"));Ya(document.getElementById("vnfs-root"));Wa(document.getElementById("settings-root"));var io=document.getElementById("client-certs-root");io&&Fe(os(As,{}),io);Ns(); +This removes the inventory record only. The disk remains available.`))try{await V(`/api/vms/${v.id}`,{method:"DELETE"}),m(""),ae(`Deleted ${v.name}`),await Promise.allSettled([B(),K(),ie()])}catch(M){m(`Could not delete ${v.name}: ${M instanceof Error?M.message:String(M)}`)}},oe=async v=>{try{let M=await V(`/api/vms/${v.id}/start`,{method:"POST"});T(t.map(k=>k.id===M.id?M:k)),m(""),ae(`Started ${v.name}`),K()}catch(M){m(`Could not start ${v.name}: ${M instanceof Error?M.message:String(M)}`),B()}},E=async v=>{try{let M=await V(`/api/vms/${v.id}/stop`,{method:"POST"});T(t.map(k=>k.id===M.id?M:k)),m(""),ae(`Stopped ${v.name}`),K()}catch(M){m(`Could not stop ${v.name}: ${M instanceof Error?M.message:String(M)}`),B()}},z=v=>{if(!v.console_enabled){ae("Console access is disabled. Edit the VM to enable it.",!0);return}if(v.status!=="running"){ae("Start the VM before opening its console.",!0);return}window.open(`/console.html?vm=${encodeURIComponent(v.id)}`,"_blank","noopener")},re=t.filter(v=>v.status==="running").length,ue=t.length===0?h?"Loading instances\u2026":"No instances registered":`${t.length} instance${t.length===1?"":"s"} \xB7 ${re} running`;return e(ge,{children:[e("section",{class:"panel","aria-labelledby":"inventory-heading",children:[e("div",{class:"panel-head",children:[e("div",{children:[e("h2",{id:"inventory-heading",children:"Inventory"}),e("p",{class:"panel-subtitle",children:ue})]}),e("button",{class:"button ghost",type:"button",disabled:h,onClick:()=>{R()},children:h?"Refreshing\u2026":"Refresh"})]}),I?e("div",{class:"banner",role:"alert",children:I}):null,e("div",{class:"table-wrap",children:t.length>0?e("table",{children:[e("thead",{children:e("tr",{children:[e("th",{scope:"col",children:"Name"}),e("th",{scope:"col",children:"Status"}),e("th",{scope:"col",children:"vCPU"}),e("th",{scope:"col",children:"Memory"}),e("th",{scope:"col",children:"Cgroup"}),e("th",{scope:"col",children:"Disks"}),e("th",{scope:"col",children:"Boot"}),e("th",{scope:"col",children:"Console"}),e("th",{scope:"col",children:"Network"}),e("th",{scope:"col",class:"hide-sm",children:"Updated"}),e("th",{scope:"col",children:e("span",{class:"sr-only",children:"Actions"})})]})}),e("tbody",{children:t.map(v=>{let M=pr(v),k=s.find(se=>se.id===v.boot_media_id)||v.boot_media,O=Array.isArray(v.interfaces)?v.interfaces:[],le=v.status==="running"||v.status==="starting";return e("tr",{children:[e("td",{children:[e("span",{class:"vm-name",children:v.name}),v.description?e("span",{class:"vm-description",title:v.description,children:v.description}):null,e(De,{entityType:"vm",entityID:v.id,name:v.name,labels:N.labels[v.id],onSaved:se=>N.setEntityLabels(v.id,se)})]}),e("td",{children:e("span",{class:"status-badge","data-status":v.status,children:v.status})}),e("td",{class:"mono",children:ao.format(v.vcpus)}),e("td",{class:"mono",children:zt(v.memory_mb)}),e("td",{class:v.cgroup_class?"mono":"mono muted",title:v.cgroup_class?`Cgroup class: ${v.cgroup_class}`:"Shared cgroup",children:v.cgroup_class||"shared"}),e("td",{class:M.length?"mono":"mono muted",title:M.length?M.map(se=>`${se.name} \xB7 ${ao.format(se.size_gb)} GB`).join(", "):void 0,children:M.length?M.map(se=>se.name).join(", "):"\u2014"}),e("td",{class:k?"mono":"mono muted",title:k?.path||void 0,children:k?k.name:"disk"}),e("td",{class:v.console_enabled?"mono":"mono muted",children:v.console_enabled?"enabled":"disabled"}),e("td",{class:O.length?"mono":"mono muted",title:O.length?O.map(se=>`${se.name} (${se.network_name})`).join(", "):void 0,children:O.length?O.map(se=>se.network_name||se.name).join(", "):"none"}),e("td",{class:"updated hide-sm",children:ur(v.updated_at)}),e("td",{children:e("div",{class:"actions",children:[e("button",{class:le?"row-action danger":"row-action",type:"button",onClick:()=>le?void E(v):void oe(v),children:le?"Stop":"Start"}),e("button",{class:"row-action",type:"button",disabled:!v.console_enabled||v.status!=="running",title:v.console_enabled?v.status!=="running"?"Start the VM to open its console.":"":"Console access is disabled. Edit the VM to enable it.",onClick:()=>z(v),children:"Console"}),e("button",{class:"row-action",type:"button",onClick:()=>te(`/vms/${v.id}/monitor`),children:"Monitor"}),e("button",{class:"row-action",type:"button",onClick:()=>te(`/vms/${v.id}/edit`),children:"Edit"}),e("button",{class:"row-action danger",type:"button",onClick:()=>{g(v)},children:"Delete"})]})})]},v.id)})})]}):e("div",{class:"empty",children:[e("div",{class:"empty-mark","aria-hidden":"true",children:"\u2B21"}),e("h3",{children:"No virtual machines yet"}),e("p",{children:"Create the first instance to start building out this host."}),e("button",{class:"button primary",type:"button",disabled:F.length===0,title:F.length===0?"Create an unattached disk on the Disks tab first.":"",onClick:Q,children:"Create a VM"})]})})]}),y?e(to,{vm:y.vm,disks:a,media:s,interfaces:f,thinPoolReady:b,vppReady:d,onClose:()=>{P(null),te("/vms",!0)},onSaved:v=>{P(null),te("/vms",!0),m(""),ae(`${y.vm?"Saved":"Created"} ${v.name}`),Promise.allSettled([B(),K(),ie()])}},y.vm?`edit-${y.vm.id}`:"new"):null,L?e(no,{vm:L,onClose:()=>{S(null),te("/vms",!0)}},`monitor-${L.id}`):null]})}function oo(t){t&&Fe(e(so,{}),t)}var io=document.getElementById("toast-root");io&&Sa(io);oo(document.getElementById("vms-root"));qa(document.getElementById("disks-root"));ns(document.getElementById("media-root"));Qa(document.getElementById("lightvm-root"));Ds(document.getElementById("networks-root"));Qs(document.getElementById("vnfs-root"));zs(document.getElementById("settings-root"));var ro=document.getElementById("client-certs-root");ro&&Fe(ia(Na,{}),ro);Da(); diff --git a/ui/src/inventory/disks/DiskDialog.tsx b/ui/src/inventory/disks/DiskDialog.tsx index 6e44d55..fcc1733 100644 --- a/ui/src/inventory/disks/DiskDialog.tsx +++ b/ui/src/inventory/disks/DiskDialog.tsx @@ -205,13 +205,14 @@ export function DiskDialog({ backends, disks, onClose, onSaved }: DiskDialogProp onChange={(event) => setStorageKind(event.currentTarget.value as "lvm" | "host")} > - Passthrough disks hand an existing host disk, partition, or logical volume to one VM. - Rookery never deletes or resizes the underlying device. + Provisioned disks use the selected storage backend. Passthrough disks hand an + existing host disk, partition, or logical volume to one VM; Rookery never deletes + or resizes the underlying device. @@ -244,14 +245,16 @@ export function DiskDialog({ backends, disks, onClose, onSaved }: DiskDialogProp > {backends.map((backend) => ( ))} - {supportsTemplates - ? "Template cloning is available on this backend." - : "Template cloning is disabled for this backend; enable it in Storage backends."} + {backend?.driver === "kouja" + ? "Kouja provisions a remote volume and exports it over the selected protocol." + : supportsTemplates + ? "Template cloning is available on this backend." + : "Template cloning is disabled for this backend; enable it in Storage backends."} {supportsTemplates ? ( diff --git a/ui/src/inventory/disks/DisksTab.tsx b/ui/src/inventory/disks/DisksTab.tsx index 45935e4..977533a 100644 --- a/ui/src/inventory/disks/DisksTab.tsx +++ b/ui/src/inventory/disks/DisksTab.tsx @@ -174,16 +174,23 @@ export function DisksTab() { const confirmed = window.confirm( disk.source === "host-device" ? `Delete disk "${disk.name}"?\n\nThis removes only the inventory record. The host block device ${disk.device} is not modified.` - : `Delete disk "${disk.name}"?\n\nThis removes the LVM thin volume and its inventory record.` + : disk.source === "kouja" + ? `Delete disk "${disk.name}"?\n\nThis queues the kouja volume for unexport and deletion. The disk stays visible until cleanup finishes.` + : `Delete disk "${disk.name}"?\n\nThis removes the LVM thin volume and its inventory record.` ); if (!confirmed) { return; } try { await api(`/api/disks/${disk.id}`, { method: "DELETE" }); - publishDisks(disks.filter((item) => item.id !== disk.id)); + if (disk.source === "kouja") { + await loadDisks(); + showToast(`Deletion queued for ${disk.name}`); + } else { + publishDisks(disks.filter((item) => item.id !== disk.id)); + showToast(`Deleted disk ${disk.name}`); + } setDiskError(""); - showToast(`Deleted disk ${disk.name}`); void loadBackends(false); } catch (err) { setDiskError(`Could not delete ${disk.name}: ${err instanceof Error ? err.message : String(err)}`); @@ -191,8 +198,8 @@ export function DisksTab() { }; const toggleTemplate = async (disk: Disk) => { - if (disk.source === "host-device") { - showToast("Host block devices cannot be templates.", true); + if (disk.source !== "lvm-thin" && disk.source) { + showToast("Only LVM thin disks can be templates.", true); return; } if (disk.attached_vm_id) { @@ -314,9 +321,11 @@ export function DisksTab() { onSaved={(labels) => backendLabels.setEntityLabels(String(backend.id), labels)} /> - {backend.driver === "lvm-thin" ? "LVM thin" : backend.driver} + {backend.driver === "lvm-thin" ? "LVM thin" : backend.driver === "kouja" ? "Kouja" : backend.driver} - {backend.config?.vg || "—"}/{backend.config?.thin_pool || "—"} + {backend.driver === "kouja" + ? `${backend.config?.kouja?.endpoint || "—"} · ${backend.config?.kouja?.storage_class || "—"}` + : `${backend.config?.vg || "—"}/${backend.config?.thin_pool || "—"}`} {backend.config?.supports_templates ? ( templates ) : null} @@ -419,10 +428,20 @@ export function DisksTab() { {disk.source === "host-device" ? ( passthrough ) : null} + {disk.source === "kouja" ? kouja : null} + {disk.deletion_state ? ( + + {disk.deletion_state === "delete_failed" ? "delete failed" : disk.deletion_state} + + ) : null} {disk.template_disk_name ? ( from {disk.template_disk_name} ) : null} - {disk.device ? ( + {disk.locator ? ( + + {disk.locator} + + ) : disk.device ? ( {disk.device} @@ -454,7 +473,7 @@ export function DisksTab() { Provisioning ) : null} - {disk.source === "host-device" ? null : ( + {disk.source === "host-device" || (disk.source && disk.source !== "lvm-thin") ? null : ( + {connectionMessage ? {connectionMessage} : null} + {connectionError ? {connectionError} : null} + + + +
+ TLS server name + +
+ +
+ CA certificate +