diff --git a/README.md b/README.md index 420a3c6..ca05d5d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ Hayduk connects to a separate Metasploit Framework instance through `msfrpcd`. I **[Download the latest release](https://github.com/jolovicdev/hayduk/releases/latest)** · [Quickstart](#quickstart) · [Try the Docker lab](#try-the-docker-lab) · [Team mode](#team-mode) -![Hayduk Metasploit GUI demo showing module selection, a detected service, and a live shell session](docs/demo.gif) +![Hayduk campaign overview with network topology, host inspection, module library, and live console](docs/screenshot.png) + +*Interface shown with illustrative campaign data.* ## Quickstart @@ -22,18 +24,7 @@ The steps below assume Hayduk and Metasploit run on the same machine. If you nee ### 1. Download and extract Hayduk -Open the [latest release](https://github.com/jolovicdev/hayduk/releases/latest) and choose the archive for your operating system and processor: - -| Your machine | Release platform | Architecture | -|---|---|---| -| Linux on Intel or AMD 64-bit | `linux` | `amd64` | -| Linux on ARM64 | `linux` | `arm64` | -| macOS on Intel | `darwin` | `amd64` | -| macOS on Apple silicon | `darwin` | `arm64` | -| Windows on Intel or AMD 64-bit | `windows` | `amd64` | -| Windows on ARM64 | `windows` | `arm64` | - -Extract the archive into a folder. There is no Hayduk installer or separate UI setup. +Open the [latest release](https://github.com/jolovicdev/hayduk/releases/latest) and choose the archive matching your operating system and processor. Extract the archive into a folder. There is no Hayduk installer or separate UI setup. ### 2. Start Metasploit RPC @@ -85,7 +76,11 @@ Keep Hayduk running while you use the UI. Press `Ctrl+C` in its terminal to stop Use a system or network you are authorized to test. Scans run from the connected Metasploit instance, so targets must be reachable from that machine. -Right-click hosts, sessions, table rows, and modules in the tree to open their action menus. +Click a module to configure it. Right-click hosts, sessions, table rows, and modules in the tree to open their action menus. + +Use the campaign summary cards to open hosts, services, sessions, or credentials. **Discover hosts**, **Scan services**, and **Export report** are also available directly above the network map. + +The network map adapts host columns to the available canvas. Choose **Focus** to expand the map and host inspector. **Arrange hosts** replaces saved positions with an automatic layout. Host cards show open service counts and access state; violet paths identify pivot routes. 1. **Choose a workspace.** Click the **workspace** chip to switch between existing Metasploit workspaces, or use the current workspace. The active workspace scopes the database tables, topology, and exported report, keeping each client's campaign data separate. Events and sessions retain their originating workspace for report attribution; the **Sessions** tab shows sessions across workspaces. 2. **Discover hosts.** Open **Campaign → Discover hosts…**, enter your target host or CIDR range, select a scanner, and click **Configure…**. Review the module options and click **Launch**. @@ -93,13 +88,15 @@ Right-click hosts, sessions, table rows, and modules in the tree to open their a 4. **Launch an exploit and open a session.** Right-click an exploit module in the tree and choose **Launch…**, or open **Campaign → Find attacks…** and click a match's **Launch** button to prefill the target host and matched port. Review the module options and payload, then click **Launch**. If a session opens, click its row in the **Sessions** tab, or right-click its host and choose **Interact with session **, to open the live console in **Interact**. 5. **Export a report.** Choose **File → Export report…** to download a self-contained HTML campaign report. -![Hayduk browser interface with network topology and Metasploit campaign controls](docs/screenshot.png) +![Hayduk demo: select a host on the topology map and run commands in its live shell session](docs/demo.gif) + +*Demo uses illustrative campaign data.* ## Features | Capability | What you can do | |---|---| -| Network topology | View hosts grouped by subnet, access states, and pivot routes; retain node positions across reloads. | +| Network topology | Explore adaptive subnet groups, host service counts, and pivot routes. Drag hosts, zoom, or expand the map in Focus mode. | | Metasploit modules | Browse the module tree, inspect reliability ranks, configure options, and select payloads. | | Campaign workflows | Discover hosts, scan services, and find exploit candidates matching known services. | | Session management | Interact with Meterpreter and shell sessions, upgrade shells, and terminate sessions. | @@ -108,9 +105,13 @@ Right-click hosts, sessions, table rows, and modules in the tree to open their a | Reporting | Export a self-contained HTML report for campaign review and client delivery. | | Team mode | Share a campaign with multiple operators on a trusted network. | +![Hayduk graph focus mode with adaptive subnet layout, host states, and a routed network](docs/topology.png) + +*Graph focus mode with illustrative campaign data.* + ## Try the Docker lab -The repository includes a disposable Metasploit lab with a database and optional target containers. The demo above uses this lab. +The repository includes a disposable Metasploit lab with a database and optional target containers. Run it to try the workflow from the demo against live targets. You need Git, Docker, and Docker Compose. Run these commands from a shell that supports the repository's `.sh` scripts: diff --git a/cmd/hayduk/main.go b/cmd/hayduk/main.go index 48b3067..b352bdf 100644 --- a/cmd/hayduk/main.go +++ b/cmd/hayduk/main.go @@ -17,7 +17,7 @@ import ( "github.com/jolovicdev/hayduk/internal/server" ) -var version = "0.1.4" +var version = "0.1.5" func main() { listen := flag.String("listen", "127.0.0.1:0", "host:port to bind") diff --git a/docs/demo.gif b/docs/demo.gif index d224107..10361ef 100644 Binary files a/docs/demo.gif and b/docs/demo.gif differ diff --git a/docs/demo.mp4 b/docs/demo.mp4 index 7440366..8b0cc18 100644 Binary files a/docs/demo.mp4 and b/docs/demo.mp4 differ diff --git a/docs/screenshot.png b/docs/screenshot.png index 2ea9248..62bfcf6 100644 Binary files a/docs/screenshot.png and b/docs/screenshot.png differ diff --git a/docs/topology.png b/docs/topology.png new file mode 100644 index 0000000..31cad9b Binary files /dev/null and b/docs/topology.png differ diff --git a/go.mod b/go.mod index 3927151..fef2689 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.1 require ( github.com/gorilla/websocket v1.5.3 - github.com/jolovicdev/go-msf/v2 v2.0.1 + github.com/jolovicdev/go-msf/v2 v2.1.0 ) require ( diff --git a/go.sum b/go.sum index f33c8e9..f781123 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/jolovicdev/go-msf/v2 v2.0.1 h1:q5fn0NOh0dFdXfYCIlnt4WWTC2FArI4NBrFmrMQidEE= -github.com/jolovicdev/go-msf/v2 v2.0.1/go.mod h1:A0bd46BNl9ncqgiZpzt17bnbSHfDkPbkpCwtaMPVHB4= +github.com/jolovicdev/go-msf/v2 v2.1.0 h1:24LegpNc4SLbZMBh9ZjzvOolkbTS6/WtsImw14bePnQ= +github.com/jolovicdev/go-msf/v2 v2.1.0/go.mod h1:A0bd46BNl9ncqgiZpzt17bnbSHfDkPbkpCwtaMPVHB4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= diff --git a/internal/engine/attacks.go b/internal/engine/attacks.go index 8d05c0e..9f8a46e 100644 --- a/internal/engine/attacks.go +++ b/internal/engine/attacks.go @@ -8,10 +8,8 @@ import ( "github.com/jolovicdev/hayduk/internal/protocol" ) -// The matcher is deliberately honest and dumb: an exploit is offered when its -// refname contains the path token of a service the host runs (windows/smb/... -// for smb). It knows nothing about versions or patch levels; the dialog copy -// says as much. +// Matches use service tokens in module paths, such as windows/smb/ for SMB. +// Versions and patch levels are not checked. const attackMatchCap = 200 diff --git a/internal/engine/commands.go b/internal/engine/commands.go index 4955577..1cc0b62 100644 --- a/internal/engine/commands.go +++ b/internal/engine/commands.go @@ -1,6 +1,7 @@ package engine import ( + "bytes" "context" "encoding/json" "errors" @@ -44,6 +45,15 @@ func (e *Engine) execCommand(ctx context.Context, operator, method string, param // input while msf still runs the command. e.mu.Lock() if e.console != nil { + // msfrpcd's web consoles do not echo commands. Store the echo + // in the engine buffer so every browser receives it on replay. + // Reuse the trailing idle prompt to avoid displaying it twice. + echo := e.consolePrompt + strings.TrimRight(p.Command, "\n") + "\n" + if e.consolePrompt != "" && bytes.HasSuffix(e.consoleOut, []byte(e.consolePrompt)) { + e.consoleOut = e.consoleOut[:len(e.consoleOut)-len(e.consolePrompt)] + } + e.consoleOut = appendCapped(e.consoleOut, []byte(echo)) + e.bus.send(protocol.ConsoleOutputMsg{Type: protocol.KindConsoleOutput, Data: echo}) e.consoleWritePending = true e.consoleWriteGen++ } @@ -291,14 +301,26 @@ func (e *Engine) moduleExecute(ctx context.Context, rpc gomsf.RPCCaller, operato } } + // Register before the RPC so sessions and job events that arrive + // before its response can be attributed to this launch. + var runKey string + if p.Type == string(gomsf.ExploitModuleType) { + runKey = e.beginExploitRun(p.Type+"/"+p.Name, ws, operator) + } var res *gomsf.ModuleExecuteResult if p.Payload != "" { payload, perr := gomsf.NewModuleWithContext(ctx, rpc, gomsf.PayloadModuleType, p.Payload) if perr != nil { + if runKey != "" { + e.forgetExploitRun(runKey) + } return nil, mapErr(perr) } for k, v := range p.PayloadOptions { if err := payload.SetOption(k, v); err != nil { + if runKey != "" { + e.forgetExploitRun(runKey) + } return nil, mapErr(err) } } @@ -307,6 +329,9 @@ func (e *Engine) moduleExecute(ctx context.Context, rpc gomsf.RPCCaller, operato res, err = mod.Execute(ctx) } if err != nil { + if runKey != "" { + e.forgetExploitRun(runKey) + } e.eventfOpIn(ws, operator, protocol.LevelError, "%s/%s failed: %v", p.Type, p.Name, err) return nil, mapErr(err) } @@ -317,6 +342,9 @@ func (e *Engine) moduleExecute(ctx context.Context, rpc gomsf.RPCCaller, operato } else { e.eventfOpIn(ws, operator, protocol.LevelSuccess, "%s/%s ran inline", p.Type, p.Name) } + if runKey != "" { + e.armExploitRun(runKey, res.UUID, res.JobID) + } return mustJSON(protocol.ExecPayload{JobID: res.JobID, UUID: res.UUID}), nil } @@ -417,10 +445,31 @@ func (e *Engine) sessionWrite(ctx context.Context, p protocol.SessionWriteParams return &protocol.ErrorBody{Code: protocol.CodeBusy, Message: "session " + p.SID + " is not attached; attach first"} } data := strings.TrimSuffix(p.Data, "\n") + "\n" + prompt := p.SID + " sh > " + if session.Type == "meterpreter" { + prompt = "meterpreter " + p.SID + " > " + } + echo := prompt + strings.TrimSuffix(p.Data, "\n") + "\n" + // Session streams do not echo commands. Store the echo in the engine + // transcript for re-attach and other operators, using the browser's + // prompt format. The echo must precede the write: the monitor can + // deliver the command's output while the write is still in flight. + e.mu.Lock() + if e.interactSID == p.SID { + e.interactOut = appendCapped(e.interactOut, []byte(echo)) + e.bus.send(protocol.SessionOutputMsg{Type: protocol.KindSessionOutput, SID: p.SID, Data: echo}) + } + e.mu.Unlock() + var err error if session.Type == "meterpreter" { - return mapErr(gomsf.NewMeterpreterSession(rpc, p.SID).Write(ctx, data)) + err = gomsf.NewMeterpreterSession(rpc, p.SID).Write(ctx, data) + } else { + err = gomsf.NewShellSession(rpc, p.SID).Write(ctx, data) + } + if err != nil { + return mapErr(err) } - return mapErr(gomsf.NewShellSession(rpc, p.SID).Write(ctx, data)) + return nil } func (e *Engine) sessionUpgrade(ctx context.Context, operator, ws string, p protocol.SessionUpgradeParams) *protocol.ErrorBody { @@ -474,6 +523,8 @@ func mapErr(err error) *protocol.ErrorBody { return &protocol.ErrorBody{Code: protocol.CodeRPC, Message: err.Error()} case errors.Is(err, gomsf.ErrUnexpectedResponse): return &protocol.ErrorBody{Code: protocol.CodeUnexpected, Message: err.Error()} + case errors.Is(err, gomsf.ErrConsoleNotFound): + return &protocol.ErrorBody{Code: protocol.CodeUnexpected, Message: err.Error()} case errors.Is(err, gomsf.ErrCommandTimeout): return &protocol.ErrorBody{Code: protocol.CodeTimeout, Message: err.Error()} case errors.Is(err, gomsf.ErrSessionNotFound): diff --git a/internal/engine/connect.go b/internal/engine/connect.go index 27e3eac..b4bb428 100644 --- a/internal/engine/connect.go +++ b/internal/engine/connect.go @@ -278,6 +278,8 @@ func (e *Engine) bootstrap(ctx context.Context, p protocol.ConnectParams, gen ui e.sessionTags[sid] = sessionTag{workspace: st.Workspace, uuid: st.UUID} } e.jobs = make(map[string]*protocol.JobState) + e.exploitRuns = nil + e.earlySessions = nil e.errStreak = 0 e.gen = gen + 1 e.mu.Unlock() @@ -340,6 +342,9 @@ func (e *Engine) Disconnect() { e.jobs = make(map[string]*protocol.JobState) e.interactSID = "" e.interactOut = nil + // Clear pending runs so their timers cannot warn after disconnect. + e.exploitRuns = nil + e.earlySessions = nil if e.conn.Status != "disconnected" { e.conn.Status = "disconnected" e.conn.Error = "" diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 49d4122..e3093ac 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -94,6 +94,11 @@ type Engine struct { // counters and get reused, so attribution carries across a reconnect // only when the session is the same one. Survives disconnects. sessionTags map[string]sessionTag + // exploitRuns is keyed by daemon job id or a synthetic inline key. + // earlySessions holds sessions awaiting a run's execution UUID. + // Both are cleared on disconnect. + exploitRuns map[string]*exploitRun + earlySessions []earlySession events []*protocol.EventEntry operators map[string]int seq int64 diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index cbdce08..8e86351 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -1410,10 +1410,25 @@ func TestConsoleWriteRestoresReadinessOnlyAfterIdleRead(t *testing.T) { t.Fatal(err) } } + // The echo precedes the idle read that restores readiness. + expectEcho := func() { + deadline := time.After(5 * time.Second) + for { + select { + case m := <-sub.C(): + out, ok := m.(protocol.ConsoleOutputMsg) + if ok && out.Data == "msf > silent-command\n" { + return + } + case <-deadline: + t.Fatal("the written command was never echoed") + } + } + } expectQuiet := func() { select { case m := <-sub.C(): - t.Fatalf("console.write broadcast %+v before an idle post-write read", m) + t.Fatalf("console.write broadcast %+v beyond the echo", m) default: } } @@ -1434,6 +1449,7 @@ func TestConsoleWriteRestoresReadinessOnlyAfterIdleRead(t *testing.T) { } write() + expectEcho() expectQuiet() e.consoleRead(con, &gomsf.ConsoleReadResult{Prompt: "msf > ", Busy: false}, e.consoleGeneration()) expectRestore() @@ -1463,6 +1479,9 @@ func TestConsoleWriteRestoresReadinessOnlyAfterIdleRead(t *testing.T) { if strings.HasSuffix(up.Data, "msf > ") { t.Fatalf("stale read streamed a ready prompt: %q", up.Data) } + if strings.Contains(up.Data, "background output") { + sawUpdate = true + } } case <-time.After(200 * time.Millisecond): goto drained @@ -1497,7 +1516,12 @@ func TestReconnectKeepsSessionAttribution(t *testing.T) { }) e := connectedEngine(t, f) - // sessions 1 and 2 open while client-alpha is active + // sessions 1 and 2 open while client-alpha is active: the daemon lists + // them, so the reconcile pass that prunes unlisted sessions keeps them + daemonSessions.Store(map[string]interface{}{ + "1": map[string]interface{}{"type": "shell", "target_host": "10.0.0.1", "uuid": "uuid-1"}, + "2": map[string]interface{}{"type": "shell", "uuid": "uuid-2"}, + }) e.mu.Lock() mon := e.monitor e.mu.Unlock() @@ -1549,6 +1573,11 @@ func TestSessionAttributionValidatesUUID(t *testing.T) { }) e := connectedEngine(t, f) + // the daemon lists session 1, so the reconcile pass that prunes + // unlisted sessions keeps it while the open event attributes it + daemonSessions.Store(map[string]interface{}{ + "1": map[string]interface{}{"type": "shell", "uuid": "uuid-old"}, + }) e.mu.Lock() mon := e.monitor e.mu.Unlock() @@ -1556,9 +1585,6 @@ func TestSessionAttributionValidatesUUID(t *testing.T) { Session: &gomsf.Session{Type: "shell", UUID: "uuid-old"}}) // same daemon, same session: the uuid matches, attribution restores - daemonSessions.Store(map[string]interface{}{ - "1": map[string]interface{}{"type": "shell", "uuid": "uuid-old"}, - }) e.Disconnect() if err := e.Connect(context.Background(), protocol.ConnectParams{}); err != nil { t.Fatalf("reconnect: %+v", err) diff --git a/internal/engine/exploitrun.go b/internal/engine/exploitrun.go new file mode 100644 index 0000000..5d8433c --- /dev/null +++ b/internal/engine/exploitrun.go @@ -0,0 +1,252 @@ +package engine + +import ( + "context" + "strconv" + "sync/atomic" + "time" + + "github.com/jolovicdev/go-msf/v2" + "github.com/jolovicdev/hayduk/internal/protocol" +) + +const ( + // inlineExploitGrace is how long an inline exploit run keeps waiting + // for its session: the module finished when module.execute answered, + // but the monitor reports opened sessions on its next poll. + inlineExploitGrace = 5 * time.Second + // jobStopGrace is how long a stopped exploit job keeps waiting before + // warning: the session poll can trail the job-stop event, and warning + // on the stop itself would flag runs whose session simply had not been + // observed yet. + jobStopGrace = 3 * time.Second + // Delay the job-list read to allow a newly launched job to appear. + jobReconcileDelay = 2 * time.Second + // earlySessionCap bounds the sessions remembered while their runs' + // execute responses were still in flight. + earlySessionCap = 32 +) + +var exploitRunSeq uint64 + +// module.execute can return job 0 without an error for a failed exploit. +// Jobs can also disappear from job.list without a stop event. Track +// sessions separately to warn when a run finishes without opening one. +type exploitRun struct { + via string // the module reference, "exploit/multi/ssh/sshexec" + // uuid is the module execution's UUID once the RPC answered; matched + // against the session's exploit_uuid so concurrent runs of the same + // module are attributed to the launch that produced them. Empty until + // the execute returns - and for daemons that do not populate it - with + // via as the fallback. + uuid string + // job is empty for inline runs. + job string + // stopping marks a run whose job-stop event already arrived; the + // job-list fallback must not warn ahead of the stop grace + stopping bool + ws string + operator string + sessionSeen bool +} + +// Sessions can open before module.execute returns the UUID needed to +// attribute them to a run. +type earlySession struct { + via string + uuid string +} + +// Register before the RPC to track sessions that precede its response. +// The key is temporary until the daemon's job id, if any, is known. +func (e *Engine) beginExploitRun(via, ws, operator string) string { + key := "inline-" + strconv.FormatUint(atomic.AddUint64(&exploitRunSeq, 1), 10) + e.mu.Lock() + if e.exploitRuns == nil { + e.exploitRuns = make(map[string]*exploitRun) + } + e.exploitRuns[key] = &exploitRun{via: via, ws: ws, operator: operator} + e.mu.Unlock() + return key +} + +// A failed launch already reports an error and must not also warn about +// a missing session. +func (e *Engine) forgetExploitRun(key string) { + e.mu.Lock() + delete(e.exploitRuns, key) + e.mu.Unlock() +} + +// Match early sessions once module.execute returns the execution UUID. +// Jobs wait for a stop event or job-list reconciliation; inline runs +// start their grace period immediately. +func (e *Engine) armExploitRun(key, uuid string, jobID int) { + e.mu.Lock() + job := "" + if run := e.exploitRuns[key]; run != nil { + run.uuid = uuid + if uuid != "" { + for i := range e.earlySessions { + if e.earlySessions[i].uuid == uuid { + run.sessionSeen = true + break + } + } + } + if jobID > 0 { + run.job = strconv.Itoa(jobID) + job = run.job + e.rekeyExploitRunLocked(key, run.job) + } + } + e.mu.Unlock() + if jobID <= 0 { + e.watchExploitRun(key, inlineExploitGrace) + return + } + e.reconcileExploitJob(job) +} + +// Jobs that start and stop between monitor polls produce no stop event. +// A missing job starts the same grace period as a stop event, allowing +// a later session poll to cancel the warning. +func (e *Engine) reconcileExploitJob(job string) { + e.mu.Lock() + gen := e.gen + want := e.exploitRuns[job] + e.mu.Unlock() + time.AfterFunc(jobReconcileDelay, func() { + rpc := e.connectedRPC() + if rpc == nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + live, err := gomsf.NewJobManager(rpc).List(ctx) + if err != nil { + return // Stop events can still resolve the run. + } + e.mu.Lock() + defer e.mu.Unlock() + if e.gen != gen { + return // Job ids may be reused after reconnect. + } + run := e.exploitRuns[job] + if run == nil || run != want || run.sessionSeen || run.stopping { + return + } + if _, listed := live[job]; !listed { + run.stopping = true + e.watchExploitRunLocked(job, jobStopGrace) + } + }) +} + +// rekeyExploitRunLocked moves a run onto its daemon job id once known. +// A pending run displaced by id reuse keeps living under the vacated key. +// Callers hold e.mu. +func (e *Engine) rekeyExploitRunLocked(from, to string) { + run := e.exploitRuns[from] + if run == nil { + return + } + delete(e.exploitRuns, from) + if incumbent := e.exploitRuns[to]; incumbent != nil { + e.exploitRuns[from] = incumbent + if !incumbent.stopping { + // Reuse proves the old job is gone without a stop event. + incumbent.stopping = true + e.watchExploitRunLocked(from, jobStopGrace) + } + } + e.exploitRuns[to] = run +} + +// The launch event reports success before the session outcome is known. +// Warn after the grace period if no session opened. Timers must match both +// the connection generation and the run because job ids can be reused +// within a connection or after reconnect. +func (e *Engine) watchExploitRun(key string, grace time.Duration) { + e.mu.Lock() + defer e.mu.Unlock() + e.watchExploitRunLocked(key, grace) +} + +// watchExploitRunLocked is watchExploitRun for callers holding e.mu. +func (e *Engine) watchExploitRunLocked(key string, grace time.Duration) { + gen := e.gen + want := e.exploitRuns[key] + if want == nil { + return + } + time.AfterFunc(grace, func() { + e.mu.Lock() + defer e.mu.Unlock() + if e.gen != gen { + return + } + for k, run := range e.exploitRuns { + if run != want { + continue + } + delete(e.exploitRuns, k) + if !run.sessionSeen { + if run.job != "" { + e.logfIn(run.ws, run.operator, protocol.LevelWarn, + "job %s (%s) finished without opening a session - check the module's options, payload, and target", + run.job, run.via) + } else { + e.logfIn(run.ws, run.operator, protocol.LevelWarn, + "%s ran without opening a session - check the module's options, payload, and target", run.via) + } + } + return + } + }) +} + +// Match sessions by exploit_uuid. Keep unmatched UUIDs until the run's +// execute response arrives. Fall back to the module reference only when +// neither side has a UUID. Callers hold e.mu. +func (e *Engine) markExploitSessionLocked(via, exploitUUID string) { + if via == "" && exploitUUID == "" { + return + } + matched := false + for _, run := range e.exploitRuns { + if exploitUUID != "" && run.uuid != "" { + if run.uuid == exploitUUID { + run.sessionSeen = true + matched = true + } + continue + } + if run.uuid == "" && exploitUUID == "" && run.via == via { + run.sessionSeen = true + matched = true + } + } + if matched || exploitUUID == "" { + return + } + for _, es := range e.earlySessions { + if es.uuid == exploitUUID { + return + } + } + e.earlySessions = append(e.earlySessions, earlySession{via: via, uuid: exploitUUID}) + if len(e.earlySessions) > earlySessionCap { + e.earlySessions = e.earlySessions[len(e.earlySessions)-earlySessionCap:] + } +} + +// finishExploitJobLocked resolves a tracked job-backed run at its stop +// event: the run waits out the stop grace for its session before the +// no-session warning fires. Callers hold e.mu. +func (e *Engine) finishExploitJobLocked(jobID string) { + if run := e.exploitRuns[jobID]; run != nil { + run.stopping = true + e.watchExploitRunLocked(jobID, jobStopGrace) + } +} diff --git a/internal/engine/exploitrun_test.go b/internal/engine/exploitrun_test.go new file mode 100644 index 0000000..372d407 --- /dev/null +++ b/internal/engine/exploitrun_test.go @@ -0,0 +1,405 @@ +package engine + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/jolovicdev/go-msf/v2" + "github.com/jolovicdev/hayduk/internal/protocol" +) + +func exploitEventText(e *Engine) []string { + e.mu.Lock() + defer e.mu.Unlock() + out := make([]string, 0, len(e.events)) + for _, ev := range e.events { + out = append(out, ev.Text) + } + return out +} + +func hasEventContaining(events []string, needle string) bool { + for _, ev := range events { + if strings.Contains(ev, needle) { + return true + } + } + return false +} + +func waitForEventContaining(t *testing.T, e *Engine, needle string, within time.Duration) { + t.Helper() + deadline := time.After(within) + for { + if hasEventContaining(exploitEventText(e), needle) { + return + } + select { + case <-deadline: + t.Fatalf("missing event %q; have %v", needle, exploitEventText(e)) + case <-time.After(20 * time.Millisecond): + } + } +} + +// execFake answers the info and option reads NewModuleWithContext makes +// plus one scripted module.execute result. +func execFake(jobID int) *fakeRPC { + fake := stdFake() + fake.set(gomsf.ModuleInfo, func(args ...interface{}) (interface{}, error) { + return map[string]interface{}{"name": args[1], "rank": "normal"}, nil + }) + fake.set(gomsf.ModuleOptions, func(args ...interface{}) (interface{}, error) { + return map[string]interface{}{}, nil + }) + fake.set(gomsf.ModuleExecute, func(args ...interface{}) (interface{}, error) { + return map[string]interface{}{"job_id": jobID, "uuid": "u"}, nil + }) + return fake +} + +func execModule(t *testing.T, e *Engine, typ, name string) *protocol.ExecPayload { + t.Helper() + raw, eb := e.Exec(context.Background(), "", protocol.MethodModuleExecute, + json.RawMessage(`{"type":"`+typ+`","name":"`+name+`"}`)) + if eb != nil { + t.Fatalf("execute: %+v", eb) + } + var res protocol.ExecPayload + if err := json.Unmarshal(raw, &res); err != nil { + t.Fatal(err) + } + return &res +} + +// The stop grace allows the session poll to catch up before a warning. +func TestExploitJobWithoutSessionWarnsOnStop(t *testing.T) { + e := connectedEngine(t, execFake(7)) + execModule(t, e, "exploit", "multi/ssh/sshexec") + + mon := e.monitor + e.jobChanged(mon, "7", "Exploit: multi/ssh/sshexec", true) + e.jobChanged(mon, "7", "Exploit: multi/ssh/sshexec", false) + + if hasEventContaining(exploitEventText(e), "without opening a session") { + t.Fatal("warning fired before the stop grace let the session poll catch up") + } + waitForEventContaining(t, e, "job 7 (exploit/multi/ssh/sshexec) finished without opening a session", + jobStopGrace+3*time.Second) +} + +// Match the session to its run by exploit_uuid. +func TestExploitJobWithSessionStaysQuiet(t *testing.T) { + e := connectedEngine(t, execFake(3)) + execModule(t, e, "exploit", "multi/ssh/sshexec") + + mon := e.monitor + e.sessionOpened(mon, gomsf.Event{SessionID: "11", Session: &gomsf.Session{ + Type: "shell", ViaExploit: "exploit/multi/ssh/sshexec", ExploitUUID: "u", TargetHost: "10.0.0.5", + }}) + e.jobChanged(mon, "3", "Exploit: multi/ssh/sshexec", true) + e.jobChanged(mon, "3", "Exploit: multi/ssh/sshexec", false) + + time.Sleep(jobStopGrace + 300*time.Millisecond) + if hasEventContaining(exploitEventText(e), "without opening a session") { + t.Fatal("a run whose session arrived must not warn") + } +} + +// A session belonging to a different run of the same module must not +// silence this run's warning. +func TestConcurrentRunsOfOneModuleAttributeByUUID(t *testing.T) { + e := connectedEngine(t, execFake(1)) + execModule(t, e, "exploit", "multi/ssh/sshexec") // job 1, uuid "u" + + mon := e.monitor + e.sessionOpened(mon, gomsf.Event{SessionID: "4", Session: &gomsf.Session{ + Type: "shell", ViaExploit: "exploit/multi/ssh/sshexec", ExploitUUID: "someone-elses-run", TargetHost: "10.0.0.6", + }}) + e.jobChanged(mon, "1", "Exploit: multi/ssh/sshexec", true) + e.jobChanged(mon, "1", "Exploit: multi/ssh/sshexec", false) + + waitForEventContaining(t, e, "job 1 (exploit/multi/ssh/sshexec) finished without opening a session", + jobStopGrace+3*time.Second) +} + +// A session arriving inside the stop grace still cancels the warning. +func TestSessionInsideStopGraceCancelsWarning(t *testing.T) { + e := connectedEngine(t, execFake(2)) + execModule(t, e, "exploit", "multi/ssh/sshexec") + + mon := e.monitor + e.jobChanged(mon, "2", "Exploit: multi/ssh/sshexec", true) + e.jobChanged(mon, "2", "Exploit: multi/ssh/sshexec", false) + // the session poll trails the job-stop event + e.sessionOpened(mon, gomsf.Event{SessionID: "9", Session: &gomsf.Session{ + Type: "shell", ViaExploit: "exploit/multi/ssh/sshexec", ExploitUUID: "u", TargetHost: "10.0.0.5", + }}) + + time.Sleep(jobStopGrace + 300*time.Millisecond) + if hasEventContaining(exploitEventText(e), "without opening a session") { + t.Fatal("a session observed inside the stop grace must cancel the warning") + } +} + +// An inline run (job 0) has no job-stop event to trigger the warning. +func TestInlineExploitWithoutSessionWarnsAfterGrace(t *testing.T) { + e := connectedEngine(t, execFake(0)) + execModule(t, e, "exploit", "multi/ssh/sshexec") + + waitForEventContaining(t, e, "exploit/multi/ssh/sshexec ran without opening a session", + inlineExploitGrace+3*time.Second) +} + +// Auxiliary launches are not exploits: no tracking, no warnings. +func TestAuxiliaryRunIsNotTracked(t *testing.T) { + e := connectedEngine(t, execFake(4)) + execModule(t, e, "auxiliary", "scanner/portscan/tcp") + e.mu.Lock() + tracked := len(e.exploitRuns) + e.mu.Unlock() + if tracked != 0 { + t.Fatalf("auxiliary launch tracked: %d runs", tracked) + } +} + +// A launch error must not also produce a missing-session warning. +func TestFailedExploitLaunchIsNotWatched(t *testing.T) { + fake := execFake(0) + fake.set(gomsf.ModuleExecute, func(args ...interface{}) (interface{}, error) { + return nil, gomsf.ErrRPC + }) + e := connectedEngine(t, fake) + + _, eb := e.Exec(context.Background(), "", protocol.MethodModuleExecute, + json.RawMessage(`{"type":"exploit","name":"multi/ssh/sshexec"}`)) + if eb == nil { + t.Fatal("execute should have failed") + } + + e.mu.Lock() + tracked := len(e.exploitRuns) + e.mu.Unlock() + if tracked != 0 { + t.Fatalf("failed launch left %d runs tracked", tracked) + } + if hasEventContaining(exploitEventText(e), "without opening a session") { + t.Fatal("failed launch must not also warn about a missing session") + } +} + +// session.write echoes the command into the authoritative transcript the +// engine replays on re-attach, with the same prompt the browser renders. +func TestSessionWriteEchoesIntoTranscript(t *testing.T) { + fake := stdFake() + var daemonSessions atomic.Value // map[string]interface{} + daemonSessions.Store(map[string]interface{}{}) + fake.set(gomsf.SessionList, func(args ...interface{}) (interface{}, error) { + return daemonSessions.Load().(interface{}), nil + }) + fake.set(gomsf.SessionShellWrite, func(args ...interface{}) (interface{}, error) { + return map[string]interface{}{"result": "success"}, nil + }) + e := connectedEngine(t, fake) + // the daemon lists the session, so the reconcile pass that prunes + // unlisted sessions keeps it + daemonSessions.Store(map[string]interface{}{ + "5": map[string]interface{}{"type": "shell"}, + }) + + e.mu.Lock() + e.sessions["5"] = &protocol.SessionState{ID: "5", Type: "shell"} + e.mu.Unlock() + if eb := e.attach("5"); eb != nil { + t.Fatalf("attach: %+v", eb) + } + + if _, eb := e.Exec(context.Background(), "", protocol.MethodSessionWrite, + json.RawMessage(`{"sid":"5","data":"id\n"}`)); eb != nil { + t.Fatalf("write: %+v", eb) + } + + e.mu.Lock() + out := string(e.interactOut) + e.mu.Unlock() + if !strings.Contains(out, "5 sh > id\n") { + t.Fatalf("transcript missing echo: %q", out) + } + + // re-attach replays the authoritative buffer, echo included + if eb := e.attach("5"); eb != nil { + t.Fatalf("re-attach: %+v", eb) + } + e.mu.Lock() + out = string(e.interactOut) + e.mu.Unlock() + if !strings.Contains(out, "5 sh > id\n") { + t.Fatalf("replay dropped the echo: %q", out) + } +} + +// The monitor can deliver a command's output while the write RPC is still +// in flight; the echo must already sit above that output in the transcript. +func TestSessionWriteEchoPrecedesRacingOutput(t *testing.T) { + fake := stdFake() + var daemonSessions atomic.Value // map[string]interface{} + daemonSessions.Store(map[string]interface{}{}) + fake.set(gomsf.SessionList, func(args ...interface{}) (interface{}, error) { + return daemonSessions.Load().(interface{}), nil + }) + var e *Engine + fake.set(gomsf.SessionShellWrite, func(args ...interface{}) (interface{}, error) { + e.mu.Lock() + mon := e.monitor + e.mu.Unlock() + e.sessionOutput(mon, gomsf.Event{SessionID: "5", Data: "uid=0 msf\n"}) + return map[string]interface{}{"result": "success"}, nil + }) + e = connectedEngine(t, fake) + daemonSessions.Store(map[string]interface{}{ + "5": map[string]interface{}{"type": "shell"}, + }) + + e.mu.Lock() + e.sessions["5"] = &protocol.SessionState{ID: "5", Type: "shell"} + e.mu.Unlock() + if eb := e.attach("5"); eb != nil { + t.Fatalf("attach: %+v", eb) + } + + if _, eb := e.Exec(context.Background(), "", protocol.MethodSessionWrite, + json.RawMessage(`{"sid":"5","data":"id\n"}`)); eb != nil { + t.Fatalf("write: %+v", eb) + } + + e.mu.Lock() + out := string(e.interactOut) + e.mu.Unlock() + echo := strings.Index(out, "5 sh > id\n") + response := strings.Index(out, "uid=0 msf") + if echo < 0 || response < 0 || echo > response { + t.Fatalf("echo must precede the racing output: %q", out) + } +} + +// A job that starts and stops between the monitor's job-list polls never +// produces a stop event; the direct list read after arming must still warn. +func TestJobVanishingBetweenPollsWarns(t *testing.T) { + e := connectedEngine(t, execFake(5)) + execModule(t, e, "exploit", "multi/ssh/sshexec") // job 5, no job events + + waitForEventContaining(t, e, "job 5 (exploit/multi/ssh/sshexec) finished without opening a session", + jobReconcileDelay+jobStopGrace+3*time.Second) +} + +// Early sessions must be attributed by UUID even when runs of the same +// module receive their execute responses out of order. +func TestEarlySessionClaimedByItsRunOnArm(t *testing.T) { + e := connectedEngine(t, stdFake()) + keyA := e.beginExploitRun("exploit/x/a", "default", "op") + keyB := e.beginExploitRun("exploit/x/a", "default", "op") + + // both runs unarmed: the sessions wait + e.mu.Lock() + e.markExploitSessionLocked("exploit/x/a", "uuid-A") + e.markExploitSessionLocked("exploit/x/a", "uuid-B") + e.mu.Unlock() + + // an unclaimed early session belongs to no run + keyC := e.beginExploitRun("exploit/x/a", "default", "op") + + e.armExploitRun(keyB, "uuid-B", 0) + e.armExploitRun(keyA, "uuid-A", 0) + e.armExploitRun(keyC, "uuid-C", 0) + + time.Sleep(inlineExploitGrace + 400*time.Millisecond) + // A and B claimed their sessions; exactly one warning - C's - remains + if countWarnings(exploitEventText(e)) != 1 { + t.Fatalf("exactly one warning (run C) expected: %v", exploitEventText(e)) + } +} + +// A daemon that reuses a job id must not lose the previous run's verdict: +// run A still warns about its missing session, and run B is tracked and +// settled on its own terms. +func TestReusedJobIDKeepsBothRunsTracked(t *testing.T) { + fake := execFake(0) + var n int + fake.set(gomsf.ModuleExecute, func(args ...interface{}) (interface{}, error) { + n++ + return map[string]interface{}{"job_id": 9, "uuid": fmt.Sprintf("u-%d", n)}, nil + }) + e := connectedEngine(t, fake) + + execModule(t, e, "exploit", "multi/ssh/sshexec") // run A, job 9 + mon := e.monitor + e.jobChanged(mon, "9", "Exploit: multi/ssh/sshexec", true) + e.jobChanged(mon, "9", "Exploit: multi/ssh/sshexec", false) + + execModule(t, e, "exploit", "multi/ssh/sshexec") // run B reuses job 9 + e.sessionOpened(mon, gomsf.Event{SessionID: "11", Session: &gomsf.Session{ + Type: "shell", ViaExploit: "exploit/multi/ssh/sshexec", ExploitUUID: "u-2", TargetHost: "10.0.0.5", + }}) + + time.Sleep(jobStopGrace + 600*time.Millisecond) // past A's stop grace + e.mu.Lock() + run := e.exploitRuns["9"] + e.mu.Unlock() + if run == nil || run.uuid != "u-2" { + t.Fatal("run A's timer resolved the replacement run under job 9") + } + // run A never opened a session, so its own warning must have fired + waitForEventContaining(t, e, "job 9 (exploit/multi/ssh/sshexec) finished without opening a session", + 3*time.Second) + + e.jobChanged(mon, "9", "Exploit: multi/ssh/sshexec", false) + time.Sleep(jobStopGrace + 300*time.Millisecond) + // run B opened a session and must stay silent: still exactly one warning + if countWarnings(exploitEventText(e)) != 1 { + t.Fatalf("run B opened a session and must not warn: %v", exploitEventText(e)) + } +} + +// A job id reused while the previous run never produced a stop event still +// settles the previous run: the reuse itself proves the old job is gone. +func TestReusedJobIDSettlesUnstoppedRun(t *testing.T) { + fake := execFake(0) + var n int + fake.set(gomsf.ModuleExecute, func(args ...interface{}) (interface{}, error) { + n++ + return map[string]interface{}{"job_id": 9, "uuid": fmt.Sprintf("u-%d", n)}, nil + }) + e := connectedEngine(t, fake) + + execModule(t, e, "exploit", "multi/ssh/sshexec") // run A, job 9, no job events + execModule(t, e, "exploit", "multi/ssh/sshexec") // run B reuses job 9 + + mon := e.monitor + e.sessionOpened(mon, gomsf.Event{SessionID: "11", Session: &gomsf.Session{ + Type: "shell", ViaExploit: "exploit/multi/ssh/sshexec", ExploitUUID: "u-2", TargetHost: "10.0.0.5", + }}) + + // run A never stopped and never opened a session: the reuse arms its + // stop grace, which must warn. Run B claimed its session and stays quiet. + waitForEventContaining(t, e, "job 9 (exploit/multi/ssh/sshexec) finished without opening a session", + jobStopGrace+3*time.Second) + time.Sleep(jobStopGrace + 300*time.Millisecond) // past B's grace, if any + if countWarnings(exploitEventText(e)) != 1 { + t.Fatalf("exactly one warning (run A) expected: %v", exploitEventText(e)) + } +} + +func countWarnings(events []string) int { + n := 0 + for _, ev := range events { + if strings.Contains(ev, "without opening a session") { + n++ + } + } + return n +} diff --git a/internal/engine/ingest.go b/internal/engine/ingest.go index c9a8199..ea8f749 100644 --- a/internal/engine/ingest.go +++ b/internal/engine/ingest.go @@ -51,8 +51,13 @@ func (e *Engine) sessionOpened(m *gomsf.EventMonitor, ev gomsf.Event) { if e.sessionTags == nil { e.sessionTags = make(map[string]sessionTag) } + exploitUUID := "" + if ev.Session != nil { + exploitUUID = ev.Session.ExploitUUID + } e.sessionTags[ev.SessionID] = sessionTag{workspace: st.Workspace, uuid: st.UUID} e.sessions[ev.SessionID] = st + e.markExploitSessionLocked(st.ViaExploit, exploitUUID) sessions := copyMap(e.sessions) host := hostLabel(st.TargetHost) e.logf(protocol.LevelSuccess, "session %s opened (%s) on %s via %s", @@ -177,6 +182,7 @@ func (e *Engine) jobChanged(m *gomsf.EventMonitor, id, name string, started bool e.jobs[id] = &protocol.JobState{ID: id, Name: name, StartedAt: time.Now().UTC()} } else { delete(e.jobs, id) + e.finishExploitJobLocked(id) } jobs := copyMap(e.jobs) if started { diff --git a/internal/server/dist/assets/index-6Qqy-GYH.css b/internal/server/dist/assets/index-6Qqy-GYH.css new file mode 100644 index 0000000..309be99 --- /dev/null +++ b/internal/server/dist/assets/index-6Qqy-GYH.css @@ -0,0 +1 @@ +@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2)format("woff2"),url(/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff)format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2)format("woff2"),url(/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff)format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2)format("woff2"),url(/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff)format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2)format("woff2"),url(/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff)format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2)format("woff2"),url(/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff)format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2)format("woff2"),url(/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff)format("woff")}@font-face{font-family:Phosphor;src:url(/assets/phosphor-regular-yS3ewmTQ.woff2)format("woff2");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Phosphor-Fill;src:url(/assets/phosphor-fill-eP-KIidR.woff2)format("woff2");font-weight:400;font-style:normal;font-display:block}.ph{font-variant:normal;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-style:normal;font-weight:400;line-height:1;font-family:Phosphor!important}.ph-fill{font-variant:normal;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-style:normal;font-weight:400;line-height:1;font-family:Phosphor-Fill!important}.ph.ph-archive:before,.ph-fill.ph-archive:before{content:""}.ph.ph-arrows-clockwise:before,.ph-fill.ph-arrows-clockwise:before{content:""}.ph.ph-arrows-out-simple:before,.ph-fill.ph-arrows-out-simple:before{content:""}.ph.ph-broadcast:before,.ph-fill.ph-broadcast:before{content:""}.ph.ph-caret-down:before,.ph-fill.ph-caret-down:before{content:""}.ph.ph-caret-right:before,.ph-fill.ph-caret-right:before{content:""}.ph.ph-check:before,.ph-fill.ph-check:before{content:""}.ph.ph-command:before,.ph-fill.ph-command:before{content:""}.ph.ph-copy:before,.ph-fill.ph-copy:before{content:""}.ph.ph-corners-out:before,.ph-fill.ph-corners-out:before{content:""}.ph.ph-crosshair:before,.ph-fill.ph-crosshair:before{content:""}.ph.ph-dots-nine:before{content:""}.ph.ph-dots-three:before{content:""}.ph-fill.ph-dots-nine:before{content:""}.ph.ph-download-simple:before,.ph-fill.ph-download-simple:before{content:""}.ph.ph-file-code:before,.ph-fill.ph-file-code:before{content:""}.ph.ph-fire:before,.ph-fill.ph-fire:before{content:""}.ph.ph-folder:before,.ph-fill.ph-folder:before{content:""}.ph.ph-folder-open:before,.ph-fill.ph-folder-open:before{content:""}.ph.ph-gear:before,.ph-fill.ph-gear:before{content:""}.ph.ph-graph:before,.ph-fill.ph-graph:before{content:""}.ph.ph-info:before,.ph-fill.ph-info:before{content:""}.ph.ph-key:before,.ph-fill.ph-key:before{content:""}.ph.ph-keyboard:before,.ph-fill.ph-keyboard:before{content:""}.ph.ph-lightning:before,.ph-fill.ph-lightning:before{content:""}.ph.ph-magnifying-glass:before,.ph-fill.ph-magnifying-glass:before{content:""}.ph.ph-minus:before,.ph-fill.ph-minus:before{content:""}.ph.ph-password:before,.ph-fill.ph-password:before{content:""}.ph.ph-pi:before,.ph-fill.ph-pi:before{content:""}.ph.ph-plug:before,.ph-fill.ph-plug:before{content:""}.ph.ph-plus:before,.ph-fill.ph-plus:before{content:""}.ph.ph-pulse:before,.ph-fill.ph-pulse:before{content:""}.ph.ph-resize:before,.ph-fill.ph-resize:before{content:""}.ph.ph-rocket-launch:before,.ph-fill.ph-rocket-launch:before{content:""}.ph.ph-signpost:before,.ph-fill.ph-signpost:before{content:""}.ph.ph-stack:before,.ph-fill.ph-stack:before{content:""}.ph.ph-table:before,.ph-fill.ph-table:before{content:""}.ph.ph-target:before,.ph-fill.ph-target:before{content:""}.ph.ph-terminal:before,.ph-fill.ph-terminal:before{content:""}.ph.ph-terminal-window:before,.ph-fill.ph-terminal-window:before{content:""}.ph.ph-tree:before,.ph-fill.ph-tree:before{content:""}.ph.ph-user:before,.ph-fill.ph-user:before{content:""}.ph.ph-users-three:before,.ph-fill.ph-users-three:before{content:""}.ph.ph-wifi-high:before,.ph-fill.ph-wifi-high:before{content:""}.ph.ph-wifi-slash:before,.ph-fill.ph-wifi-slash:before{content:""}.ph.ph-x:before,.ph-fill.ph-x:before{content:""}:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--accent:#c4edac;--accent-dim:#c4edac1a;--accent-ink:#192416;--accent-hi:#d5f6c3;--bg0:#101516;--bg1:#191f21;--bg2:#242d30;--bg3:#303c3f;--well:#12191b;--cardline:#2d393c;--line:#3a484c;--tx0:#edf2ee;--tx1:#afbfbe;--tx2:#8b9e9e;--red:#c9404a;--red-hi:#d54954;--red-br:#e2666b;--red-dim:#c9404a29;--grn:#3fa87c;--grn-tx:#55c78f;--grn-dim:#3fa87c29;--live-line:#45533f;--live-fill:#242e22;--route:#b1a3ed;--route-dim:#b1a3ed1a;--amb:#e3b76a;--amb-dim:#d9a13f24;--sans:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",sans-serif;--mono:"IBM Plex Mono",ui-monospace,"Cascadia Mono",Menlo,Consolas,monospace;--shadow:0 16px 46px #00000080}*{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}body{background:var(--bg0);color:var(--tx0);font:400 13px/1.5 var(--sans);-webkit-font-smoothing:antialiased;padding:0 20px 10px;overflow:hidden}button{font:inherit;color:inherit;cursor:pointer;background:0 0;border:0}button:disabled{opacity:.4;cursor:not-allowed}input[type=checkbox]{accent-color:var(--accent)}:focus-visible{outline:2px solid var(--accent);outline-offset:1px;border-radius:4px}::selection{background:var(--accent-dim)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--line);border:3px solid var(--bg1);border-radius:6px}::-webkit-scrollbar-track{background:0 0}*{scrollbar-width:thin;scrollbar-color:var(--line) transparent}i.ph,i.ph-fill{flex:none;justify-content:center;align-items:center;width:16px;height:16px;font-size:16px;display:inline-flex}kbd{font:500 10.5px var(--mono);background:var(--well);border:1px solid var(--line);color:var(--tx1);border-bottom-width:2px;border-radius:4px;padding:2px 6px}.app{grid-template-columns:var(--w-left,274px) 4px minmax(0,1fr) 4px var(--w-right,314px);grid-template-rows:60px 90px auto minmax(220px,1fr) 4px var(--h-nb,214px) auto;grid-template-areas:"men men men men men""tool tool tool tool tool""overview overview overview overview overview""left lsp stage rsp right""nsp nsp nsp nsp nsp""nb nb nb nb nb""st st st st st";gap:10px;height:calc(100dvh - 10px);display:grid}.splitter{z-index:40;background:0 0;border-radius:2px;transition:background .12s}.splitter:hover,.splitter.active{background:var(--line)}.splitter.lsp{cursor:col-resize;grid-area:lsp;margin:-2px -8px}.splitter.rsp{cursor:col-resize;grid-area:rsp;margin:-2px -8px}.splitter.nsp{cursor:row-resize;grid-area:nsp;margin:-8px -2px}.card{background:var(--bg1);border:1px solid var(--cardline);border-radius:12px;min-width:0;min-height:0}.menubar{border:0;border-bottom:1px solid var(--line);-webkit-user-select:none;user-select:none;background:0 0;border-radius:0;grid-area:men;align-items:center;gap:2px;height:60px;padding:0 12px;display:flex}.brand{align-items:center;gap:10px;margin-right:16px;display:flex}.brand svg{display:block}.brand .word{font:600 28px var(--sans);letter-spacing:-1.5px}.mbtn{height:29px;font:500 12.5px var(--sans);color:var(--tx1);border-radius:6px;padding:0 11px}.mbtn:hover:not(:disabled),.mbtn.open{background:var(--bg2);color:var(--tx0)}.menubar .spacer{flex:1}.chip{border:1px solid var(--line);background:var(--bg2);height:24px;font:500 11px var(--sans);color:var(--tx1);border-radius:4px;align-items:center;gap:6px;padding:0 9px;display:inline-flex}.chip b{color:var(--tx0);font-weight:600}.toolbar{-webkit-user-select:none;user-select:none;grid-area:tool;align-items:center;gap:6px;height:72px;padding:0 12px;display:flex}.tdiv{background:var(--line);width:1px;height:20px;margin:0 5px}.toolbar .spacer{flex:1}.left{flex-direction:column;grid-area:left;display:flex}.panelhead{align-items:center;gap:9px;padding:16px 16px 10px;display:flex}.panelhead>i{color:var(--tx2);width:14px;height:14px;font-size:14px}.panelhead .pt{font:600 13px var(--sans);color:var(--tx0)}.panelhead .pc{font:500 10.5px var(--mono);color:var(--tx1);background:var(--bg2);border:1px solid var(--line);border-radius:4px;margin-left:auto;padding:2px 7px}.tree{-webkit-user-select:none;user-select:none;flex:1;padding:2px 8px 14px;overflow-y:auto}.tree ul{list-style:none}.trow{width:100%;height:32px;font:400 12px var(--mono);color:var(--tx0);text-align:left;border-radius:6px;align-items:center;gap:7px;padding:0 8px;display:flex}.trow:hover{background:var(--bg2)}.trow .caret,.trow .fic{color:var(--tx2);width:14px;height:14px;font-size:14px}.trow .tlabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.trow .cnt{font:400 10px var(--mono);color:var(--tx2);background:var(--bg2);border-radius:4px;flex:none;margin-left:auto;padding:1px 6px}.trow.leaf{padding-left:37px}.trow.leaf .mfil{color:var(--tx2);width:14px;height:14px;font-size:14px}.trow .rankchip{font:500 10px var(--mono);letter-spacing:.05em;color:var(--tx2);background:var(--bg2);border-radius:4px;flex:none;margin-left:auto;padding:1px 5px}.trow .rankchip.hot{color:var(--red-br);border:1px solid var(--red);padding:0 4px}.tree .noresult{font:400 12px var(--sans);color:var(--tx2);padding:16px 12px;display:none}.tree.filtered .noresult{display:block}.tree .filterbox{margin:0 0 10px}.stage{flex-direction:column;grid-area:stage;display:flex}.stagehead{-webkit-user-select:none;user-select:none;align-items:center;gap:10px;height:56px;padding:0 12px;display:flex}.stagebody{background:var(--well);border-radius:0 0 12px 12px;flex:1;margin:0;position:relative;overflow:hidden;container:mapstage/inline-size}.view{position:absolute;inset:0}.view[hidden]{display:none}#topo{cursor:default;touch-action:none;-webkit-user-select:none;user-select:none;width:100%;height:calc(100% - 82px);display:block;position:absolute;inset:44px 0 38px}#topo.panning{cursor:grabbing}.svcwrap{padding:4px 8px 16px;position:absolute;inset:0;overflow:auto}.right{flex-direction:column;grid-area:right;display:flex}.ins{flex:1;padding:14px 16px 20px;overflow-y:auto}.ihost{justify-content:space-between;align-items:flex-start;gap:10px;display:flex}.iname{font:600 15px var(--sans)}.ios{font:400 12px var(--sans);color:var(--tx1);align-items:center;gap:6px;margin-top:4px;display:flex}.ios i{width:14px;height:14px;color:var(--tx2);font-size:14px}.badges{flex-wrap:wrap;gap:6px;margin-top:12px;display:flex}.inote{background:var(--well);border:1px solid var(--cardline);font:400 12px/1.55 var(--sans);color:var(--tx1);border-radius:6px;margin-top:12px;padding:9px 11px}.isec{margin-top:20px}.isec .ilabel{font:600 11px var(--sans);letter-spacing:.02em;color:var(--tx1);margin-bottom:8px}.kv{justify-content:space-between;gap:10px;padding:4px 0;display:flex}.kv b{font:400 12px var(--sans);color:var(--tx2);font-weight:400}.kv span{font:500 12px var(--mono)}.nb{flex-direction:column;grid-area:nb;display:flex}.nbtabs{-webkit-user-select:none;user-select:none;align-items:center;gap:4px;padding:9px 10px 0;display:flex}.nbtab{height:36px;font:500 12px var(--sans);color:var(--tx1);border-radius:6px 6px 0 0;align-items:center;gap:8px;padding:0 12px;display:inline-flex}.nbtab i{width:14px;height:14px;font-size:14px}.nbtab:hover:not(:disabled){background:var(--bg2);color:var(--tx0)}.nbtab.on{background:var(--accent-dim);color:var(--accent);box-shadow:inset 0 -2px var(--accent)}.nbbody{background:var(--well);border-radius:6px;flex:1;min-height:0;margin:8px;position:relative;overflow:hidden}.nbpane{position:absolute;inset:0;overflow:auto}.nbpane[hidden]{display:none}.nbpane .dtwrap{padding:2px 6px 14px}.status{height:32px;font:400 11.5px var(--sans);color:var(--tx1);-webkit-user-select:none;user-select:none;grid-area:st;align-items:center;gap:10px;padding:0 14px;display:flex}.status i.wifi{color:var(--grn)}.status .addr{font:400 11px var(--mono);color:var(--tx2)}.status .spacer{flex:1}.status .ver{font:400 10.5px var(--mono);color:var(--tx2)}.brand-dot,.heading-dot{color:var(--accent)}.brand-caption{max-width:85px;font:500 10px/1.5 var(--mono);letter-spacing:.12em;color:var(--tx2);border-left:1px solid var(--line);margin-left:4px;padding-left:12px}.workspace-chip{border-radius:6px;height:32px}.eyebrow{color:var(--tx2);font:500 10px var(--mono);letter-spacing:.14em;margin-bottom:7px}.campaign-heading h1{font:500 26px/1.2 var(--sans);letter-spacing:-.7px}.campaign-heading p{color:var(--tx2);margin-top:6px;font-size:12px}.overview{grid-area:overview;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;padding-bottom:6px;display:grid}.metric{text-align:left;background:var(--bg1);border:1px solid var(--cardline);border-radius:12px;align-items:center;gap:12px;min-width:0;min-height:64px;padding:12px 16px;display:flex;position:relative}.metric:hover:not(:disabled){border-color:var(--tx2);background:var(--bg2)}.metric-icon{background:var(--bg2);border:1px solid var(--line);width:32px;height:32px;color:var(--tx1);border-radius:12px;flex:none;place-items:center;display:grid}.metric-icon i{font-size:16px}.metric-label{color:var(--tx1);font-size:12px}.metric strong{font:500 24px/1.2 var(--mono);letter-spacing:-1px;margin-left:auto}.metric.live{border-color:var(--live-line);background:linear-gradient(110deg,var(--live-fill),var(--bg1))}.metric.live .metric-icon{color:var(--accent);background:var(--accent-dim);border-color:var(--live-line)}.metric.live strong{color:var(--accent)}.panel-description{color:var(--tx2);margin:0 16px 14px;font-size:11px}.panel-kicker{font:400 10px var(--mono);letter-spacing:.1em;color:var(--tx2);margin-left:auto}.stage-title{margin-right:auto;font-weight:500}.stagehead .spacer{flex:0}.map-empty{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:12px;padding:24px;display:flex;position:absolute;inset:0}.map-empty h2,.ins-empty h2,.empty-state h2{font:500 18px var(--sans)}.map-empty p,.ins-empty p,.empty-state p{color:var(--tx2);max-width:310px;font-size:12px}.empty-symbol{width:52px;height:52px;color:var(--accent);background:var(--accent-dim);border:1px solid var(--live-line);border-radius:12px;place-items:center;margin-bottom:8px;display:grid}.empty-symbol i{font-size:20px}.ins-empty{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:12px;height:100%;min-height:220px;display:flex}.empty-state{text-align:center;font-family:var(--sans);flex-direction:column;justify-content:center;align-items:center;gap:12px;min-height:180px;padding:28px 24px;display:flex}.status.card{background:0 0;border:0;height:24px;padding:0 4px}@media (min-width:1001px) and (max-height:850px){.app{grid-template-rows:52px 80px auto minmax(180px,1fr) 4px min(var(--h-nb),160px) auto;gap:8px}.menubar{height:52px}.toolbar{height:80px}.campaign-heading h1{font-size:25px}.metric{padding:10px 16px}.metric strong{font-size:23px}}@media (min-width:1001px) and (max-height:650px){body{overflow:auto}.app{min-height:640px}}@media (max-width:1250px){.app{grid-template-columns:230px 4px minmax(0,1fr) 4px 260px}.brand-caption,.stage-title{display:none}.stagehead .spacer{flex:1}}@media (max-width:1000px){body{padding:0 12px 12px;overflow:auto}.app{grid-template:"men men"60px"tool tool""overview overview""left stage"420px"right nb"280px"st st"". ."/230px minmax(0,1fr);height:auto}.splitter{display:none}.toolbar{flex-wrap:wrap;gap:8px;height:auto;padding:18px 0}.campaign-heading{width:100%;margin-bottom:8px}.toolbar .spacer{display:none}.metric{gap:9px;padding:12px}.metric-icon{display:none}.nbtabs{flex-shrink:0;overflow-x:auto}.nbtab{flex-shrink:0}}@media (max-width:600px){.app{grid-template:"men""tool""overview""stage"400px"right"280px"left"320px"nb"320px"st"/minmax(0,1fr)}.menubar{flex-wrap:wrap;gap:4px;height:auto;padding:12px 0}.brand{margin-right:auto}.menubar .spacer{display:none}.workspace-chip{width:100%;margin-top:6px}.overview{grid-template-columns:repeat(2,minmax(0,1fr))}.metric-icon{display:none}.toolbar .tbtn{padding:0 10px;font-size:11px}.status{flex-wrap:wrap;height:auto!important}.status .addr,.status .ver{display:none}.legend{flex-wrap:wrap;gap:8px;padding-right:42px;font-size:9px}}.graph-focus{border:1px solid var(--line);height:28px;color:var(--tx1);border-radius:6px;align-items:center;gap:6px;padding:0 8px;font-size:11px;display:flex}.graph-focus:hover:not(:disabled),.graph-focus[aria-pressed=true]{background:var(--accent-dim);color:var(--accent);border-color:var(--live-line)}.app.graph-focused{grid-template-rows:60px minmax(0,1fr) 24px;grid-template-columns:minmax(0,1fr) 4px var(--w-right);grid-template-areas:"men men men""stage rsp right""st st st";height:calc(100dvh - 10px)}.graph-focused .toolbar,.graph-focused .overview,.graph-focused .left,.graph-focused .nb,.graph-focused .lsp,.graph-focused .nsp{display:none}@media (max-width:1000px){.app.graph-focused{grid-template:"men""stage"minmax(0,1fr)"st"24px/minmax(0,1fr)}.graph-focused .right,.graph-focused .rsp{display:none}}@media (max-width:600px){.stagehead{gap:6px;padding:0 8px}.seg button{gap:4px;padding:0 8px;font-size:11px}.zoomui .zbtn{width:23px}.graph-focus{flex-shrink:0;gap:4px;padding:0 6px}.legend{align-content:center;gap:4px 10px;padding-right:48px}.legend span{white-space:nowrap}}.tbtn{border:1px solid var(--line);background:var(--bg2);height:36px;font:500 12.5px var(--sans);border-radius:6px;align-items:center;gap:8px;padding:0 15px;transition:background .12s,transform 50ms;display:inline-flex}.tbtn i{color:var(--tx1);width:14px;height:14px;font-size:14px}.tbtn:hover:not(:disabled){background:var(--bg3)}.tbtn:hover i{color:var(--tx0)}.tbtn:active{transform:translateY(1px)}.tbtn.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}.tbtn.primary i{color:var(--accent-ink)}.tbtn.primary:hover:not(:disabled){background:var(--accent-hi);border-color:var(--accent-hi)}.tbtn .badge,.nbtab .badge{background:var(--red-dim);min-width:17px;height:16px;color:var(--red-br);font:600 10px var(--mono);border-radius:4px;justify-content:center;align-items:center;padding:0 4px;display:inline-flex}.nbtab .badge.jobs{background:var(--grn-dim);color:var(--grn-tx)}.filterbox{position:relative}.filterbox>i{color:var(--tx2);width:14px;height:14px;font-size:14px;position:absolute;top:8px;left:10px}.filterbox input{background:var(--well);border:1px solid var(--line);width:100%;height:31px;font:400 12px var(--mono);color:var(--tx0);border-radius:6px;padding:0 9px 0 31px}.filterbox input::placeholder{color:var(--tx2);font-family:var(--sans)}.filterbox input:focus{border-color:var(--red);outline:none}.menuwrap{position:relative}.dropdown{z-index:80;background:var(--bg2);border:1px solid var(--line);min-width:238px;box-shadow:var(--shadow);border-radius:10px;padding:6px;position:absolute;top:calc(100% + 5px);left:0}.ditem{width:100%;height:31px;font:400 12.5px var(--sans);color:var(--tx0);text-align:left;border-radius:6px;align-items:center;gap:10px;padding:0 10px;display:flex}.ditem i{color:var(--tx1)}.ditem:hover:not(:disabled){background:var(--bg3)}.ditem .kbd{margin-left:auto}.ditem .tick{visibility:hidden;color:var(--grn-tx);margin-left:auto}.ditem.on .tick{visibility:visible}.ditem.danger,.ditem.danger i{color:var(--red-br)}.dsep{background:var(--line);height:1px;margin:6px 8px}.seg{background:var(--well);border:1px solid var(--line);border-radius:6px;gap:2px;padding:2px;display:flex}.seg button{height:26px;font:500 12px var(--sans);color:var(--tx1);border-radius:6px;align-items:center;gap:7px;padding:0 12px;display:inline-flex}.seg button i{width:14px;height:14px;font-size:14px}.seg button:hover:not(:disabled){color:var(--tx0)}.seg button.on{background:var(--bg3);color:var(--tx0)}.zoomui{align-items:center;gap:2px;display:flex}.zbtn{width:27px;height:27px;color:var(--tx1);border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.zbtn:hover:not(:disabled){background:var(--bg2);color:var(--tx0)}.zpct{text-align:center;min-width:46px;font:500 11px var(--mono);color:var(--tx1)}.stagebody.gridbg #topo{background-image:radial-gradient(circle,#344244 1px,#0000 1px);background-size:24px 24px}#topo{background-color:var(--well);shape-rendering:geometricprecision}#topo text{-webkit-user-select:none;user-select:none;pointer-events:none}#topo .topo-zone-panel{fill:#172123;fill-opacity:.85;stroke:#344649;stroke-width:1px}#topo .topo-zone-rule{stroke:#2c3b3e;stroke-width:1px}#topo .topo-zone-icon-bg{fill:#243335}#topo .topo-zone-icon{fill:#8da8a7}#topo .topo-zone-link{fill:none;stroke:#8da8a7;stroke-width:1.25px}#topo .topo-zone-name{fill:#c2d5d1;font:500 12px var(--mono);letter-spacing:.03em}#topo .topo-zone-meta{fill:#91a5a4;font:500 10px var(--mono);letter-spacing:.08em}#topo .topo-zone-route-note{fill:var(--route);font:500 10px var(--mono);letter-spacing:.06em}#topo .topo-route-port{fill:var(--route);stroke:var(--well);stroke-width:3px}#topo .topo-route-halo,#topo .topo-route-line{fill:none;stroke-linejoin:round}#topo .topo-route-halo{stroke:var(--well);stroke-width:7px}#topo .topo-route-line{stroke:var(--route);stroke-width:1.6px;stroke-dasharray:5 5;stroke-linecap:round}#topo #topo-route-arrow path{fill:var(--route)}#topo .topo-route-label rect{fill:#262536;stroke:#655d83}#topo .topo-route-label text{fill:#c4b8f6;text-anchor:middle;font:500 10px var(--mono)}#topo .topo-routes g,#topo .topo-ghost{cursor:context-menu}#topo .topo-node{cursor:grab;outline:none}#topo .topo-node:active{cursor:grabbing}#topo .topo-node-shell{fill:#243034;stroke:#46585c;stroke-width:1px;filter:url(#topo-node-shadow);transition:stroke .15s,fill .15s}#topo .topo-node:hover .topo-node-shell{fill:#2b393b;stroke:#8aaba6}#topo .topo-node.access .topo-node-shell{fill:#233631;stroke:#577d66}#topo .topo-node.login .topo-node-shell{stroke:#88744f}#topo .topo-node.selected .topo-node-shell,#topo .topo-node:focus-visible .topo-node-shell{stroke:var(--accent);stroke-width:2px;fill:#2c4037}#topo .topo-node-divider{stroke:#49605c;stroke-width:1px;opacity:.5}#topo .topo-device-bg{fill:#33464b;stroke:#526a70}#topo .topo-device-screen,#topo .topo-device-stand{fill:none;stroke:#b3c9cc;stroke-width:1.4px;stroke-linecap:round;stroke-linejoin:round}#topo .topo-device.win .topo-device-bg{fill:#283c50;stroke:#426684}#topo .topo-device.win .topo-device-screen,#topo .topo-device.win .topo-device-stand{stroke:#8ebee5}#topo .topo-device.linux .topo-device-bg{fill:#3c3c2e;stroke:#6d6b48}#topo .topo-device.linux .topo-device-screen,#topo .topo-device.linux .topo-device-stand{stroke:#d0c88b}#topo .topo-node-os{fill:#a9babb;font:500 10px var(--mono);letter-spacing:.08em}#topo .topo-node-os.win{fill:#91bde0}#topo .topo-node-os.linux{fill:#d0c88b}#topo .topo-node-name{fill:#f1f5ef;font:600 15px var(--sans)}#topo .topo-node-address{fill:#b8ccca;font:400 12px var(--mono)}#topo .topo-host-status{fill:#90a5a9}#topo .topo-host-state,#topo .topo-host-services{fill:#a7bcbb;font:400 10px var(--sans)}#topo .access .topo-host-status,#topo .access .topo-host-state{fill:var(--accent)}#topo .login .topo-host-status,#topo .login .topo-host-state{fill:var(--amb)}#topo .topo-ghost-shell{fill:#242434;stroke:#756995;stroke-width:1px;stroke-dasharray:5 4}#topo .topo-ghost-icon-bg{fill:#322e46;stroke:#5e527d}#topo .topo-ghost-icon{fill:var(--route)}#topo .topo-ghost-link{fill:none;stroke:var(--route);stroke-width:1.3px}#topo .topo-ghost-kicker{fill:#a79acb;font:500 10px var(--mono);letter-spacing:.07em}#topo .topo-ghost-address{fill:#d5c9fa;font:500 12px var(--mono)}#topo .topo-ghost-meta{fill:#a79acb;font:400 10px var(--mono)}.map-context{height:44px;font:400 11px var(--mono);color:var(--tx2);border-bottom:1px solid var(--cardline);justify-content:space-between;align-items:center;padding:0 20px;display:flex;position:absolute;inset:0 0 auto}.map-context>span{align-items:center;gap:8px;display:flex}.map-context-dot{background:var(--accent);border-radius:50%;width:5px;height:5px}.map-context-divider{color:var(--line);margin:0 4px}.map-context button{font:400 11px var(--sans);border-radius:6px;align-items:center;gap:6px;height:30px;padding:0 8px;display:flex}.map-context button:hover:not(:disabled){background:var(--bg2);color:var(--tx0)}.map-context button i{width:14px;height:14px;font-size:14px}.map-scale{font:500 10px var(--mono);color:var(--tx2);position:absolute;bottom:12px;right:16px}.legend{border-top:1px solid var(--cardline);height:38px;font:400 11px var(--sans);color:var(--tx1);-webkit-user-select:none;user-select:none;pointer-events:none;align-items:center;gap:16px;padding:0 16px;display:flex;position:absolute;inset:auto 0 0}.legend i.sw{vertical-align:0;border-radius:50%;width:6px;height:6px;margin-right:6px;display:inline-block}.legend .dot{background:var(--accent)}.legend .sq{background:var(--amb)}.legend .neutral{background:var(--tx2)}.legend .dash{border-top:2px dashed var(--route);border-radius:0;width:12px;height:0}table.dt{border-collapse:collapse;width:100%}table.dt th{z-index:2;background:var(--well);font:600 10.5px var(--sans);letter-spacing:.08em;text-align:left;color:var(--tx2);border-bottom:1px solid var(--line);padding:10px 12px 8px;position:sticky;top:0}table.dt td{white-space:nowrap;padding:10px 12px;font-size:12.5px}table.dt td.m{font:400 12px var(--mono);color:var(--tx1)}table.dt td b{font-weight:600}table.dt tbody tr{cursor:default}table.dt tbody tr:hover{background:var(--bg1)}table.dt tbody tr.sel{background:var(--bg2)}table.dt .st{color:var(--grn-tx);font-family:var(--mono);font-size:11.5px}table.dt .dim{color:var(--tx1)}.bdg{height:23px;font:500 11px var(--mono);border-radius:4px;align-items:center;gap:6px;padding:0 9px;display:inline-flex}.bdg i{width:12px;height:12px;font-size:12px}.bdg.red{background:var(--red-dim);color:var(--red-br)}.bdg.grn{background:var(--grn-dim);color:var(--grn-tx)}.bdg.amb{background:var(--amb-dim);color:var(--amb)}.bdg.gry{background:var(--bg2);color:var(--tx1)}.portrow{align-items:baseline;gap:9px;padding:5px 0;display:flex}.pchip{font:500 11px var(--mono);color:var(--tx1);background:var(--well);border:1px solid var(--cardline);border-radius:4px;flex:none;padding:2px 7px}.portrow .pn{font:500 12.5px var(--sans)}.portrow .pi{font:400 11px var(--mono);color:var(--tx2);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.acc{border:1px solid var(--cardline);background:var(--well);border-radius:6px;padding:12px}.acc .aline{font:400 11.5px var(--mono);color:var(--tx1);word-break:break-all;margin-top:7px}.acc .aline:first-of-type{margin-top:0}.acc .aline b{color:var(--tx0);font-weight:500}.abtns{gap:6px;margin-top:12px;display:flex}.abtn{border:1px solid var(--line);background:var(--bg2);height:29px;font:500 12px var(--sans);border-radius:6px;flex:1;justify-content:center;align-items:center;gap:7px;display:inline-flex}.abtn i{width:14px;height:14px;color:var(--tx1);font-size:14px}.abtn:hover:not(:disabled){background:var(--bg3)}.abtn:active{transform:translateY(1px)}.console{height:100%;font:400 12px/1.7 var(--mono);padding:12px 16px 14px;overflow:auto}.console .cl{white-space:pre;color:var(--tx0)}.console .cl.out{color:var(--tx1)}.console .cl.ok{color:var(--grn-tx)}.console .cl.dim{color:var(--tx2)}.console .inputline{white-space:pre;align-items:center;display:flex}.console .inputline .pr{color:var(--tx1)}.console input{min-width:60px;font:400 12px var(--mono);color:var(--tx0);caret-color:var(--red-br);background:0 0;border:0;outline:0;flex:1}.console input::placeholder{color:var(--tx2)}.events{padding:6px 4px}.evline{border-radius:6px;gap:14px;padding:6px 14px;display:flex}.evline:hover{background:var(--bg1)}.evline time{font:400 11px var(--mono);color:var(--tx2);flex:none;padding-top:1px}.evline p{font:400 12.5px var(--sans);color:var(--tx1)}.evline p b{color:var(--tx0);font-weight:600}.ctx{z-index:300;background:var(--bg2);border:1px solid var(--line);min-width:238px;box-shadow:var(--shadow);border-radius:10px;padding:6px;display:none;position:fixed}.ctx.show{display:block}.chead{padding:8px 10px 7px}.chead .chn{font:600 12.5px var(--sans)}.chead .chi{font:400 10.5px var(--mono);color:var(--tx2);margin-top:1px}.citem{width:100%;height:31px;font:400 12.5px var(--sans);color:var(--tx0);text-align:left;border-radius:6px;align-items:center;gap:10px;padding:0 10px;display:flex}.citem i{color:var(--tx1)}.citem:hover:not(:disabled){background:var(--bg3)}.citem.danger,.citem.danger i{color:var(--red-br)}.citem .hint{font:400 10.5px var(--mono);color:var(--tx2);margin-left:auto}.csep{background:var(--line);height:1px;margin:6px 8px}#statusflash{color:var(--red-br);opacity:0;transition:opacity .25s}#statusflash.show{opacity:1}.modalback{z-index:200;-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);background:#080c09c7;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal{background:var(--bg1);border:1px solid var(--line);width:420px;max-width:calc(100vw - 40px);max-height:calc(100vh - 80px);box-shadow:var(--shadow);border-radius:12px;padding:24px 26px 22px;overflow:auto}.modal .mhead{align-items:center;gap:12px;display:flex}.modal .mhead svg.mark{flex:none;width:28px;height:28px}.modal .mtitle{font:600 17px var(--sans)}.modal p{font:400 12.5px/1.65 var(--sans);color:var(--tx1);margin-top:14px}.modal .mbtns{justify-content:flex-end;margin-top:20px;display:flex}.modal .mbtns .abtn{flex:none;padding:0 20px}.klist{flex-direction:column;gap:11px;margin-top:16px;display:flex}.krow{font:400 12.5px var(--sans);color:var(--tx1);align-items:center;gap:12px;display:flex}.krow .keys{gap:4px;min-width:86px;display:flex}.modal input[type=text],.modal input[type=password],.modal input[type=number],.modal input:not([type]),.modal select{background:var(--well);border:1px solid var(--line);font:400 12px var(--mono);color:var(--tx0);border-radius:6px;width:100%;padding:10px 12px}.modal .kv{grid-template-columns:minmax(0,1fr);align-content:start;gap:4px;display:grid}.modal input.invalid{border-color:var(--red-br)}.modal .kv b{font:500 11px var(--sans);color:var(--tx1)}.fmatch{background:var(--well);border:1px solid var(--line);border-radius:6px;align-items:center;gap:10px;padding:7px 10px;display:flex}.fmatch .fminfo{flex:1;gap:2px;min-width:0;display:grid}.fmatch .fmname{font:400 12px var(--mono);color:var(--tx0);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.fmatch .fmreason{font:400 11px var(--sans);color:var(--tx2)}.events .opchip{font:500 10.5px var(--mono);letter-spacing:.05em;color:var(--tx2);border:1px solid var(--line);vertical-align:1px;border-radius:4px;margin-left:8px;padding:0 5px}@media (prefers-reduced-motion:reduce){*,:before,:after{transition:none!important;animation:none!important}}.connect-modal{width:440px}.ins .isec{border-top:1px solid var(--cardline);padding-top:14px}.ins .iname{letter-spacing:-.5px;font-size:17px}.trow.expanded,.trow.expanded .fic{color:var(--accent)}.filterbox input:focus{border-color:var(--accent)}.map-context-hint{color:var(--amb);font:500 10px var(--mono);letter-spacing:.02em}.fselect{gap:6px;display:grid}.fselect input,.fselect select{padding:8px 12px}.rowmenu{width:28px;height:26px;color:var(--tx2);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;justify-content:center;align-items:center;display:inline-flex}.rowmenu i{font-size:16px}.rowmenu:hover:not(:disabled),.rowmenu:focus-visible{background:var(--bg2);color:var(--tx0);border-color:var(--line)}.opt-skeleton{align-content:start;gap:12px;min-height:170px;margin-top:16px;display:grid}.opt-skeleton .skel{background:var(--bg2);border-radius:6px;height:38px;animation:1.1s ease-in-out infinite skel-pulse}.opt-skeleton .skel.short{width:55%}.opt-skeleton.tight{min-height:0}@keyframes skel-pulse{0%,to{opacity:.45}50%{opacity:1}}#topo.lod .topo-hosts{display:none}#topo .topo-chips .topo-zone-chip{cursor:pointer}#topo .topo-chips .topo-zone-chip-name{fill:#c2d5d1;font:600 12px var(--mono)}#topo .topo-chips .topo-zone-chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:8px}#topo .topo-chips .topo-zone-chip:hover .topo-zone-chip-name,#topo .topo-chips .topo-zone-chip:focus-visible .topo-zone-chip-name{fill:var(--tx0)}#topo .topo-zone-chip:hover .topo-zone-chip-hit,#topo .topo-zone-chip:focus-visible .topo-zone-chip-hit{fill:#ffffff0d}.row-info{text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom;max-width:260px;color:var(--tx2);display:inline-block;overflow:hidden}@container mapstage (width<=560px){.map-context>span:first-child{display:none}}@container mapstage (width<=400px){.map-context>span.map-context-hint{display:none}} diff --git a/internal/server/dist/assets/index-Cmu2DH4u.js b/internal/server/dist/assets/index-Cmu2DH4u.js new file mode 100644 index 0000000..64a036c --- /dev/null +++ b/internal/server/dist/assets/index-Cmu2DH4u.js @@ -0,0 +1,4 @@ +var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r};(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var n={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return r(this.context.count)},getNextContextId(){return r(this.context.count++)}};function r(e){let t=String(e),r=t.length-1;return n.context.id+(r?String.fromCharCode(96+r):``)+t}function i(e){n.context=e}var a=(e,t)=>e===t,o=Symbol(`solid-track`),s={equals:a},c=null,l=F,u=1,d=2,f={owned:null,cleanups:null,context:null,owner:null},p={},m=null,h=null,g=null,_=null,v=null,y=0;function b(e,t){let n=g,r=m,i=e.length===0,a=t===void 0?r:t,o=i?f:{owned:null,cleanups:null,context:a?a.context:null,owner:a},s=i?e:()=>e(()=>E(()=>R(o)));m=o,g=null;try{return P(s,!0)}finally{g=n,m=r}}function x(e,t){t=t?Object.assign({},s,t):s;let n={value:e,observers:null,observerSlots:null,comparator:t.equals||void 0};return[j.bind(n),e=>(typeof e==`function`&&(e=h&&h.running&&h.sources.has(n)?e(n.tValue):e(n.value)),oe(n,e))]}function ee(e,t,n){M(ce(e,t,!0,u))}function S(e,t,n){M(ce(e,t,!1,u))}function C(e,t,n){l=I;let r=ce(e,t,!1,u),i=ae&&ie(ae);i&&(r.suspense=i),(!n||!n.render)&&(r.user=!0),v?v.push(r):M(r)}function w(e,t,n){n=n?Object.assign({},s,n):s;let r=ce(e,t,!0,0);return r.observers=null,r.observerSlots=null,r.comparator=n.equals||void 0,M(r),j.bind(r)}function te(e){return e&&typeof e==`object`&&`then`in e}function T(e,t,r){let i,a,o;typeof t==`function`?(i=e,a=t,o=r||{}):(i=!0,a=e,o=t||{});let s=null,c=p,l=null,u=!1,d=!1,f=`initialValue`in o,_=typeof i==`function`&&w(i),v=new Set,[y,b]=(o.storage||x)(o.initialValue),[S,C]=x(void 0),[T,D]=x(void 0,{equals:!1}),[O,re]=x(f?`ready`:`unresolved`);m&&k(()=>{for(let e of v.keys())e.decrement();v.clear(),h&&s&&h.promises.delete(s),s=null}),n.context&&(l=n.getNextContextId(),o.ssrLoadFrom===`initial`?c=o.initialValue:n.load&&n.has(l)&&(c=n.load(l)));function A(e,t,n,r){return s===e&&(s=null,r!==void 0&&(f=!0),(e===c||t===c)&&o.onHydrated&&queueMicrotask(()=>o.onHydrated(r,{value:t})),c=p,h&&e&&u?(h.promises.delete(e),u=!1,P(()=>{h.running=!0,j(t,n)},!1)):j(t,n)),t}function j(e,t){P(()=>{t===void 0&&b(()=>e),re(t===void 0?f?`ready`:`unresolved`:`errored`),C(t);for(let e of v.keys())e.decrement();v.clear()},!1)}function oe(){let e=ae&&ie(ae),t=y(),n=S();if(n!==void 0&&!s)throw n;return g&&!g.user&&e&&ee(()=>{T(),s&&(e.resolved&&h&&u?h.promises.add(s):v.has(e)||(e.increment(),v.add(e)))}),t}function M(e=!0){if(e!==!1&&d)return;d=!1;let t=_?_():i;if(u=h&&h.running,t==null||t===!1){A(s,E(y));return}h&&s&&h.promises.delete(s);let n,r=c===p?E(()=>{try{return a(t,{value:y(),refetching:e})}catch(e){n=e}}):c;if(n!==void 0){A(s,void 0,fe(n),t);return}return te(r)?(s=r,`v`in r?(r.s===1?A(s,r.v,void 0,t):A(s,void 0,fe(r.v),t),r):(d=!0,queueMicrotask(()=>d=!1),P(()=>{re(f?`refreshing`:`pending`),D()},!1),r.then(e=>A(r,e,void 0,t),e=>A(r,void 0,fe(e),t)))):(A(s,r,void 0,t),r)}Object.defineProperties(oe,{state:{get:()=>O()},error:{get:()=>S()},loading:{get(){let e=O();return e===`pending`||e===`refreshing`}},latest:{get(){if(!f)return oe();let e=S();if(e&&!s)throw e;return y()}}});let se=m;return _?ee(()=>(se=m,M(!1))):M(!1),[oe,{refetch:e=>ne(se,()=>M(e)),mutate:b}]}function E(e){if(g===null)return e();let t=g;g=null;try{return e()}finally{g=t}}function D(e,t,n){let r=Array.isArray(e),i,a=n&&n.defer;return n=>{let o;if(r){o=Array(e.length);for(let t=0;tt(o,i,n));return i=o,s}}function O(e){C(()=>E(e))}function k(e){return m===null||(m.cleanups===null?m.cleanups=[e]:m.cleanups.push(e)),e}function ne(e,t){let n=m,r=g;m=e,g=null;try{return P(t,!0)}catch(e){me(e)}finally{m=n,g=r}}var[re,A]=x(!1);function ie(e){let t;return m&&m.context&&(t=m.context[e.id])!==void 0?t:e.defaultValue}var ae;function j(){let e=h&&h.running;if(this.sources&&(e?this.tState:this.state)){if((e?this.tState:this.state)===u)M(this);else{let e=_;_=null,P(()=>L(this),!1),_=e}}if(g){let e=this.observers;if(!e||e[e.length-1]!==g){let t=e?e.length:0;g.sources?(g.sources.push(this),g.sourceSlots.push(t)):(g.sources=[this],g.sourceSlots=[t]),e?(e.push(g),this.observerSlots.push(g.sources.length-1)):(this.observers=[g],this.observerSlots=[g.sources.length-1])}}return e&&h.sources.has(this)?this.tValue:this.value}function oe(e,t,n){let r=h&&h.running&&h.sources.has(e)?e.tValue:e.value;if(!e.comparator||!e.comparator(r,t)){if(h){let r=h.running;(r||!n&&h.sources.has(e))&&(h.sources.add(e),e.tValue=t),r||(e.value=t)}else e.value=t;e.observers&&e.observers.length&&P(()=>{for(let t=0;t1e6)throw _=[],Error()},!1)}return t}function M(e){if(!e.fn)return;R(e);let t=y;se(e,h&&h.running&&h.sources.has(e)?e.tValue:e.value,t),h&&!h.running&&h.sources.has(e)&&queueMicrotask(()=>{P(()=>{h&&(h.running=!0),g=m=e,se(e,e.tValue,t),g=m=null},!1)})}function se(e,t,n){let r,i=m,a=g;g=m=e;try{r=e.fn(t)}catch(t){return e.pure&&(h&&h.running?(e.tState=u,e.tOwned&&e.tOwned.forEach(R),e.tOwned=void 0):(e.state=u,e.owned&&e.owned.forEach(R),e.owned=null)),e.updatedAt=n+1,me(t)}finally{g=a,m=i}(!e.updatedAt||e.updatedAt<=n)&&(e.updatedAt!=null&&`observers`in e?oe(e,r,!0):h&&h.running&&e.pure?(h.sources.has(e)||(e.value=r),h.sources.add(e),e.tValue=r):e.value=r,e.updatedAt=n)}function ce(e,t,n,r=u,i){let a={fn:e,state:r,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:t,owner:m,context:m?m.context:null,pure:n};return h&&h.running&&(a.state=0,a.tState=r),m===null||m!==f&&(h&&h.running&&m.pure?m.tOwned?m.tOwned.push(a):m.tOwned=[a]:m.owned?m.owned.push(a):m.owned=[a]),a}function N(e){let t=h&&h.running;if((t?e.tState:e.state)===0)return;if((t?e.tState:e.state)===d)return L(e);if(e.suspense&&E(e.suspense.inFallback))return e.suspense.effects.push(e);let n=[e];for(;(e=e.owner)&&(!e.updatedAt||e.updatedAt=0;r--){if(e=n[r],t){let t=e,i=n[r+1];for(;(t=t.owner)&&t!==i;)if(h.disposed.has(t))return}if((t?e.tState:e.state)===u)M(e);else if((t?e.tState:e.state)===d){let t=_;_=null,P(()=>L(e,n[0]),!1),_=t}}}function P(e,t){if(_)return e();let n=!1;t||(_=[]),v?n=!0:v=[],y++;try{let t=e();return le(n),t}catch(e){n||(v=null),_=null,me(e)}}function le(e){if(_&&=(F(_),null),e)return;let t;if(h){if(!h.promises.size&&!h.queue.size){let e=h.sources,n=h.disposed;v.push.apply(v,h.effects),t=h.resolve;for(let e of v)`tState`in e&&(e.state=e.tState),delete e.tState;h=null,P(()=>{for(let e of n)R(e);for(let t of e){if(t.value=t.tValue,t.owned)for(let e=0,n=t.owned.length;el(n),!1),t&&t()}function F(e){for(let t=0;t=0;t--)R(e.tOwned[t]);delete e.tOwned}if(h&&h.running&&e.pure)de(e,!0);else if(e.owned){for(t=e.owned.length-1;t>=0;t--)R(e.owned[t]);e.owned=null}if(e.cleanups){for(t=e.cleanups.length-1;t>=0;t--)e.cleanups[t]();e.cleanups=null}h&&h.running?e.tState=0:e.state=0}function de(e,t){if(t||(e.tState=0,h.disposed.add(e)),e.owned)for(let t=0;t1?[]:null;return k(()=>ge(a)),()=>{let l=e()||[],u=l.length,d,f;return l[o],E(()=>{let e,t,o,m,h,g,_,v,y;if(u===0)s!==0&&(ge(a),a=[],r=[],i=[],s=0,c&&=[]),n.fallback&&(r=[he],i[0]=b(e=>(a[0]=e,n.fallback())),s=1);else if(s===0){for(i=Array(u),f=0;f=g&&v>=g&&r[_]===l[v];_--,v--)o[v]=i[_],m[v]=a[_],c&&(h[v]=c[_]);for(e=new Map,t=Array(v+1),f=v;f>=g;f--)y=l[f],d=e.get(y),t[f]=d===void 0?-1:d,e.set(y,f);for(d=g;d<=_;d++)y=r[d],f=e.get(y),f!==void 0&&f!==-1?(o[f]=i[d],m[f]=a[d],c&&(h[f]=c[d]),f=t[f],e.set(y,f)):a[d]();for(f=g;fe(t||{}))}var ve=0;function ye(){return n.context?n.getNextContextId():`cl-${ve++}`}var be=e=>`Stale read from <${e}>.`;function B(e){let t=`fallback`in e&&{fallback:()=>e.fallback};return w(_e(()=>e.each,e.children,t||void 0))}function V(e){let t=e.keyed,n=w(()=>e.when,void 0,void 0),r=t?n:w(n,void 0,{equals:(e,t)=>!e==!t});return w(()=>{let i=r();if(i){let a=e.children;return typeof a==`function`&&a.length>0?E(()=>a(t?i:()=>{if(!E(r))throw be(`Show`);return n()})):a}return e.fallback},void 0,void 0)}var H=e=>w(()=>e());function xe(e,t,n){let r=n.length,i=t.length,a=r,o=0,s=0,c=t[i-1].nextSibling,l=null;for(;or-s){let i=t[o];for(;s{i=r,t===document?e():K(t,e(),t.firstChild?null:void 0,n)},r.owner),()=>{i(),t.textContent=``}}function U(e,t,n,r){let i,a=()=>{let t=r?document.createElementNS(`http://www.w3.org/1998/Math/MathML`,`template`):document.createElement(`template`);return t.innerHTML=e,n?t.content.firstChild.firstChild:r?t.firstChild:t.content.firstChild},o=t?()=>E(()=>document.importNode(i||=a(),!0)):()=>(i||=a()).cloneNode(!0);return o.cloneNode=o,o}function W(e,t=window.document){let n=t[Se]||(t[Se]=new Set);for(let r=0,i=e.length;rr.call(e,n[1],t))}else e.addEventListener(t,n,typeof n!=`function`&&n)}function Ee(e,t,n){if(!t)return n?G(e,`style`):t;let r=e.style;if(typeof t==`string`)return r.cssText=t;typeof n==`string`&&(r.cssText=n=void 0),n||={},t||={};let i,a;for(a in n)t[a]??r.removeProperty(a),delete n[a];for(a in t)i=t[a],i!==n[a]&&(r.setProperty(a,i),n[a]=i);return n}function De(e,t,n){n==null?e.style.removeProperty(t):e.style.setProperty(t,n)}function Oe(e,t,n){return E(()=>e(t,n))}function K(e,t,n,r){if(n!==void 0&&!r&&(r=[]),typeof t!=`function`)return je(e,t,r,n);S(r=>je(e,t(),r,n),r)}function ke(e){return!!n.context&&!n.done&&(!e||e.isConnected)}function Ae(e){if(n.registry&&n.events&&n.events.find(([t,n])=>n===e))return;let t=e.target,r=`$$${e.type}`,i=e.target,a=e.currentTarget,o=t=>Object.defineProperty(e,"target",{configurable:!0,value:t}),s=()=>{let n=t[r];if(n&&!t.disabled){let i=t[`${r}Data`];if(i===void 0?n.call(t,e):n.call(t,i,e),e.cancelBubble)return}return t.host&&typeof t.host!=`string`&&!t.host._$host&&t.contains(e.target)&&o(t.host),!0},c=()=>{for(;s()&&(t=t._$host||t.parentNode||t.host););};if(Object.defineProperty(e,"currentTarget",{configurable:!0,get(){return t||document}}),n.registry&&!n.done&&(n.done=_$HY.done=!0),e.composedPath){let n=e.composedPath();o(n[0]);for(let e=0;e{let i=t();for(;typeof i==`function`;)i=i();n=je(e,i,n,r)}),()=>n;else if(Array.isArray(t)){let o=[],c=n&&Array.isArray(n);if(Me(o,t,n,i))return S(()=>n=je(e,o,n,r,!0)),()=>n;if(a){if(!o.length)return n;if(r===void 0)return n=[...e.childNodes];let t=o[0];if(t.parentNode!==e)return n;let i=[t];for(;(t=t.nextSibling)!==r;)i.push(t);return n=i}if(o.length===0){if(n=q(e,n,r),s)return n}else c?n.length===0?Ne(e,o,r):xe(e,n,o):(n&&q(e),Ne(e,o));n=o}else if(t.nodeType){if(a&&t.parentNode)return n=s?[t]:t;if(Array.isArray(n)){if(s)return n=q(e,n,r,t);q(e,n,null,t)}else n==null||n===``||!e.firstChild?e.appendChild(t):e.replaceChild(t,e.firstChild);n=t}return n}function Me(e,t,n,r){let i=!1;for(let a=0,o=t.length;a=0;a--){let o=t[a];if(i!==o){let t=o.parentNode===e;!r&&!a?t?e.replaceChild(i,o):e.insertBefore(i,n):t&&o.remove()}else r=!0}}else e.insertBefore(i,n);return[i]}var Pe=U(``),Fe=U(`<svg viewBox="0 0 24 24"><rect x=2 y=2 width=20 height=20 rx=5 style=fill:var(--accent)></rect><g style=fill:var(--accent-ink)><rect x=7.2 y=6.8 width=2.6 height=10.4></rect><rect x=14.2 y=6.8 width=2.6 height=10.4></rect><rect x=7.2 y=10.8 width=9.6 height=2.4>`);function Ie(e){let t=()=>e.size??24;return(()=>{var n=Fe(),r=n.firstChild;return K(n,z(V,{get when(){return e.title},get children(){var t=Pe();return K(t,()=>e.title),t}}),r),S(r=>{var i=t(),a=t(),o=e.title?void 0:`true`,s=e.title?`img`:void 0,c=!e.tile;return i!==r.e&&G(n,`width`,r.e=i),a!==r.t&&G(n,`height`,r.t=a),o!==r.a&&G(n,`aria-hidden`,r.a=o),s!==r.o&&G(n,`role`,r.o=s),c!==r.i&&n.classList.toggle(`mark`,r.i=c),r},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0}),n})()}var J=class extends Error{code;constructor(e,t){super(t),this.code=e}},Le=1;function Re(e=location){return`${e.protocol===`https:`?`wss`:`ws`}://${e.host}/ws`}var ze={value:``};function Be(e){ze.value=e}function Ve(){return ze.value}function He(){let e=null,t=1,n=500,r=new Map,i=new Map,a={status:`connecting`};function o(){a.status=`connecting`,e=new WebSocket(Re());let t=!1;e.addEventListener(`open`,()=>{a.status=`open`,n=500,i.get(`open`)?.forEach(e=>e({}))}),e.addEventListener(`message`,n=>{let a;try{a=JSON.parse(n.data)}catch{console.warn(`unparseable ws message`,n.data);return}if(a?.type===`hello`){if(typeof a.proto==`number`&&a.proto!==Le){t=!0,i.get(`proto_mismatch`)?.forEach(e=>e({proto:a.proto,version:a.version}));try{e?.close()}catch{}return}i.get(`hello`)?.forEach(e=>e(a));return}if(a?.type===`response`){let e=r.get(a.id);if(r.delete(a.id),!e)return;a.ok?e.resolve(a.data):e.reject(new J(a.error?.code??`internal`,a.error?.message??``));return}a?.type?i.get(a.type)?.forEach(e=>e(a)):console.warn(`ws message without type`,a)}),e.addEventListener(`close`,()=>{a.status=`closed`;let e=new J(`disconnected`,`connection to hayduk lost`);for(let t of r.values())t.reject(e);r.clear(),i.get(`closed`)?.forEach(e=>e({})),t||(setTimeout(o,n),n=Math.min(n*2,8e3))})}return o(),{status:()=>a.status,command(n,i){let a=t++;return new Promise((t,o)=>{if(!e||e.readyState!==1){o(new J(`disconnected`,`connection to hayduk is not open`));return}r.set(a,{resolve:t,reject:o});try{e.send(JSON.stringify({type:`command`,id:a,method:n,params:i,...ze.value?{operator:ze.value}:{}}))}catch(e){r.delete(a),o(new J(`disconnected`,`send failed: ${e?.message??e}`))}})},on(e,t){let n=i.get(e);return n||(n=new Set,i.set(e,n)),n.add(t),()=>n.delete(t)}}}var Ue=t({ws:()=>Y}),Y=He(),[We,Ge]=x([]),[Ke,qe]=x(!1),Je=0,Ye=2e3;function Xe(e){Je&&e.seq!==Je+1&&qe(!0),Je=e.seq,Ge(t=>{let n=[...t,{seq:e.seq,time:e.time||new Date().toISOString(),level:e.level,text:e.text,...e.operator?{operator:e.operator}:{}}];return n.length>Ye?n.slice(n.length-Ye):n})}function Ze(e){Je=e?.at(-1)?.seq??0,qe(!1),Ge(e??[])}Y.on(`event`,Xe),Y.on(`snapshot`,e=>Ze(e.state?.events));var Qe={host:`127.0.0.1`,port:55553,ssl:!1,username:`msf`};function $e(e){if(!/^\d+$/.test(e.trim()))return;let t=Number(e.trim());return t>=1&&t<=65535?t:void 0}function et(e=localStorage){let t={...Qe};for(let n of Object.keys(Qe)){let r=e.getItem(`hayduk.`+n);if(r!==null){if(n===`port`){let e=$e(r);e!==void 0&&(t[n]=e)}else t[n]=n===`ssl`?r===`true`:r}}return t}function tt(e){let t=e.trim(),n=t.toLowerCase();return n.includes(`login failed`)?{primary:`Login failed - msfrpcd rejected the username or password.`,detail:t}:/connection refused|no connection could be made/.test(n)?{primary:`Connection refused - nothing is answering at that host and port; is msfrpcd running?`,detail:t}:/timeout|timed out|deadline exceeded/.test(n)?{primary:`Timed out reaching msfrpcd - check the host, port, and SSL setting.`,detail:t}:/tls|ssl|handshake|http response to https/.test(n)?{primary:`Protocol mismatch - try toggling the SSL option to match how msfrpcd runs.`,detail:t}:{primary:t,detail:``}}var nt=U(`<div style=margin-top:16px;display:grid;gap:8px;text-align:center><p style=margin:0;color:var(--tx0)>Connecting to <b>:</b>…</p><p style="margin:0;font:400 11.5px var(--mono);color:var(--tx2)"></p><p style="margin:6px 0 0;font:400 11px var(--sans);color:var(--tx2)">a cold msfrpcd can take half a minute to answer; the link state is always in the status bar`),rt=U(`<div class="modalback show"><form class="modal connect-modal"aria-labelledby=connect-title><div class=mhead><div><div class=eyebrow>METASPLOIT CONNECTION</div><div class=mtitle id=connect-title>Connect to msfrpcd</div></div></div><p>Enter the host, port, and credentials for your Metasploit RPC server.`),it=U(`<div style=margin-top:16px;display:grid;gap:10px><label class=kv><b>Host</b><input></label><div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"><label class=kv><b>Port</b><input inputmode=numeric></label><label class=kv><b>User</b><input></label></div><label class=kv><b>Password</b><input type=password></label><label style="display:flex;gap:8px;align-items:center;font:400 12.5px var(--sans);color:var(--tx1)"><input type=checkbox>use SSL (msfrpcd without -S)`),at=U(`<div class=mbtns><button class=abtn type=submit style="flex:none;padding:0 20px">`),ot=U(`<p role=alert style=color:var(--red-br);margin-top:12px>`),st=U(`<p style="margin-top:2px;font:400 10.5px var(--mono);color:var(--tx2);word-break:break-all">`);function ct(e){let t=et(),[n,r]=x({...t,port:String(t.port)}),[i,a]=x(``),[o,s]=x(``),[c,l]=x(!1),u=()=>e.conn.status===`connecting`,d=()=>$e(n().port)!==void 0,f=()=>n().host.trim()!==``&&d()&&i()!==``,p=()=>u()?We().at(-1)?.text??`reaching msfrpcd`:``;async function m(t){t.preventDefault();let r=$e(n().port);if(u()||c()||!f()||r===void 0)return;l(!0),s(``);let a={...n(),host:n().host.trim(),port:r,password:i()};for(let e of Object.keys(Qe))localStorage.setItem(`hayduk.`+e,String(a[e]));let o;try{await Promise.race([e.onConnect(a),new Promise((e,t)=>{o=window.setTimeout(()=>t(Error(`timeout`)),12e4)})])}catch(e){s(e.message??String(e))}finally{o!==void 0&&window.clearTimeout(o),l(!1)}}return(()=>{var t=rt(),s=t.firstChild,l=s.firstChild,h=l.firstChild;return l.nextSibling,s.addEventListener(`submit`,m),K(l,z(Ie,{size:30}),h),K(s,z(V,{get when(){return u()},get fallback(){return[(()=>{var e=it(),t=e.firstChild,o=t.firstChild.nextSibling,s=t.nextSibling,c=s.firstChild,l=c.firstChild.nextSibling,u=c.nextSibling.firstChild.nextSibling,f=s.nextSibling,p=f.firstChild.nextSibling,m=f.nextSibling.firstChild;return o.$$input=e=>r({...n(),host:e.currentTarget.value}),l.$$input=e=>r({...n(),port:e.currentTarget.value}),u.$$input=e=>r({...n(),username:e.currentTarget.value}),p.$$input=e=>a(e.currentTarget.value),m.addEventListener(`change`,e=>r({...n(),ssl:e.currentTarget.checked})),S(()=>l.classList.toggle(`invalid`,!d())),S(()=>o.value=n().host),S(()=>l.value=n().port),S(()=>u.value=n().username),S(()=>p.value=i()),S(()=>m.checked=n().ssl),e})(),z(V,{get when(){return e.conn.error||o()},children:t=>{let n=tt(e.conn.error||o());return[(()=>{var e=ot();return K(e,()=>n.primary),e})(),z(V,{get when(){return n.detail},get children(){var e=st();return K(e,()=>n.detail),e}})]}}),(()=>{var e=at(),t=e.firstChild;return K(t,()=>c()?`Connecting…`:`Connect`),S(()=>t.disabled=c()||!f()),e})()]},get children(){var t=nt(),n=t.firstChild,r=n.firstChild.nextSibling,i=r.firstChild,a=n.nextSibling;return K(r,()=>e.conn.host,i),K(r,()=>e.conn.port,null),K(a,p),t}}),null),t})()}W([`input`]);var lt=U(`<div class=ctx id=ctx role=menu>`);function ut(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`)}function dt(e){return e.map(e=>e.sep?`<div class="csep"></div>`:e.head?`<div class="chead"><div class="chn">${ut(e.head)}</div>`+(e.sub?`<div class="chi">${ut(e.sub)}</div>`:``)+`</div>`:`<button class="citem${e.danger?` danger`:``}" role="menuitem">`+(e.icon?`<i class="ph ph-${ut(e.icon)}"></i>`:`<i></i>`)+`<span>${ut(e.label??``)}</span>`+(e.hint?`<span class="hint">${ut(e.hint)}</span>`:``)+`</button>`).join(``)}var X=null,ft=null;function pt(){X?.classList.remove(`show`),X?.contains(document.activeElement)&&ft?.focus(),ft=null}function mt(){return X?.classList.contains(`show`)??!1}function ht(e,t,n,r,i,a){return{x:Math.max(0,Math.min(e,i-n-8)),y:Math.max(0,Math.min(t,a-r-8))}}function gt(e,t){e.preventDefault(),e.stopImmediatePropagation(),_t(e.clientX,e.clientY,t)}function _t(e,t,n){if(!X)return;ft=document.activeElement instanceof HTMLElement?document.activeElement:null,X.innerHTML=dt(n);let r=0,i=n.filter(e=>e.label!==void 0);X.querySelectorAll(`button.citem`).forEach(e=>{let t=i[r++];t?.fn&&e.addEventListener(`click`,()=>{pt(),t.fn()})}),X.classList.add(`show`);let a=X.offsetWidth,o=X.offsetHeight,s=ht(e,t,a,o,window.innerWidth,window.innerHeight);X.style.left=s.x+`px`,X.style.top=s.y+`px`}function vt(){let e;return O(()=>{X=e;let t=e=>{e.target.closest(`#ctx`)||pt()};document.addEventListener(`click`,t),document.addEventListener(`contextmenu`,t),window.addEventListener(`blur`,pt),window.addEventListener(`resize`,pt),k(()=>{document.removeEventListener(`click`,t),document.removeEventListener(`contextmenu`,t),window.removeEventListener(`blur`,pt),window.removeEventListener(`resize`,pt)})}),(()=>{var t=lt(),n=e;return typeof n==`function`?Oe(n,t):e=t,t})()}var yt=U(`<div class="dropdown show"role=menu>`),bt=U(`<div class=menuwrap><button class=mbtn aria-haspopup=true>`),xt=U(`<i>`),St=U(`<span class=kbd>`),Ct=U(`<button><span>`);function wt(e){let[t,n]=x(!1),r;return O(()=>{let e=e=>{t()&&r&&!r.contains(e.target)&&n(!1)},i=e=>{e.key===`Escape`&&n(!1)};document.addEventListener(`click`,e),document.addEventListener(`keydown`,i),k(()=>{document.removeEventListener(`click`,e),document.removeEventListener(`keydown`,i)})}),(()=>{var i=bt(),a=i.firstChild,o=r;return typeof o==`function`?Oe(o,i):r=i,a.$$click=e=>{e.stopPropagation(),n(!t())},K(a,()=>e.label),K(i,z(V,{get when(){return t()},get children(){var t=yt();return t.$$click=()=>n(!1),K(t,()=>e.children),t}}),null),S(()=>a.classList.toggle(`open`,!!t())),i})()}function Z(e){return(()=>{var t=Ct(),n=t.firstChild;return t.$$click=()=>e.onClick?.(),K(t,z(V,{get when(){return e.icon},get children(){var t=xt();return S(()=>we(t,`ph ph-${e.icon}`)),t}}),n),K(n,()=>e.label),K(t,z(V,{get when(){return e.hint},get children(){var t=St();return K(t,()=>e.hint),t}}),null),S(n=>{var r=`ditem${e.danger?` danger`:``}`,i=e.disabled;return r!==n.e&&we(t,n.e=r),i!==n.t&&(t.disabled=n.t=i),n},{e:void 0,t:void 0}),t})()}W([`click`]);function Tt(e){return t=>{t.key===`Escape`&&(e.overlayOpen?.()||e.onClose())}}var Et=U(`<div class="modalback show"><div class=modal role=dialog aria-modal=true tabindex=-1><div class=mhead><div><div class=mtitle></div></div><button class=zbtn aria-label=Close style=margin-left:auto><i class="ph ph-x">`);function Dt(e){let t,n=ye();return O(()=>{let n=document.activeElement instanceof HTMLElement?document.activeElement:null;t?.focus(),k(()=>n?.focus());let r=Tt({onClose:e.onClose,overlayOpen:mt});document.addEventListener(`keydown`,r),k(()=>document.removeEventListener(`keydown`,r));let i=e=>{if(e.key!==`Tab`||!t)return;let n=[...t.querySelectorAll(`button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])`)].filter(e=>!e.hasAttribute(`disabled`));if(n.length===0)return;let r=n[0],i=n[n.length-1],a=document.activeElement;e.shiftKey&&(a===r||a===t||!t.contains(a))?(e.preventDefault(),i.focus()):!e.shiftKey&&(a===i||a===t||!t.contains(a))&&(e.preventDefault(),r.focus())};document.addEventListener(`keydown`,i,!0),k(()=>document.removeEventListener(`keydown`,i,!0))}),(()=>{var r=Et(),i=r.firstChild,a=i.firstChild.firstChild,o=a.firstChild,s=a.nextSibling;r.$$click=t=>{t.target===t.currentTarget&&e.onClose()};var c=t;return typeof c==`function`?Oe(c,i):t=i,G(i,`aria-labelledby`,n),G(o,`id`,n),K(o,()=>e.title),Te(s,`click`,e.onClose,!0),K(i,()=>e.children,null),S(t=>Ee(i,e.width?`width:${e.width}`:``,t)),r})()}W([`click`]);function Ot(e,t,n){return{name:e,path:t,type:n,count:0,children:[]}}var kt={exploits:`exploit`,auxiliary:`auxiliary`,post:`post`,payloads:`payload`,encoders:`encoder`,nops:`nop`,evasion:`evasion`};function At(e){let t=Ot(``,``,``);for(let[n,r]of Object.entries(e)){let e=kt[n];if(!e)continue;let i=Ot(n,e,e);for(let t of r??[]){let n=t.split(`/`),r=i;r.count++;for(let i=0;i<n.length;i++)if(i===n.length-1)r.children.push(Ot(n[i],t,e));else{let t=r.children.find(e=>e.name===n[i]&&e.children.length>0);t||(t=Ot(n[i],r.path===``?n[i]:`${r.path}/${n[i]}`,e),r.children.push(t)),t.count++,r=t}}jt(i),t.children.push(i)}return t.children.sort((e,t)=>e.name.localeCompare(t.name)),t}function jt(e){e.children.sort((e,t)=>e.name.localeCompare(t.name));for(let t of e.children)jt(t)}function Mt(e,t){return t?e:[]}function Nt(e,t){let n=new Set;if(!t)return n;let r=t.toLowerCase();function i(e){let t=!1;for(let n of e.children)t=i(n)||t;let a=e.path.toLowerCase().includes(r);return(t||a)&&n.add(e.path),t||a}return i(e),n}function Pt(e){switch((e??``).toLowerCase()){case`excellent`:return{label:`excellent`,hot:!0};case`great`:return{label:`great`,hot:!0};case`good`:case`average`:case`low`:case`manual`:return{label:e.toLowerCase(),hot:!1};default:return null}}var Ft;function Q(e){let t=document.getElementById(`statusflash`);t&&(t.textContent=e,t.classList.add(`show`),window.clearTimeout(Ft),Ft=window.setTimeout(()=>t.classList.remove(`show`),2600))}async function It(e){if(navigator.clipboard&&window.isSecureContext)try{return await navigator.clipboard.writeText(e),!0}catch{}try{let t=document.createElement(`textarea`);t.value=e,t.setAttribute(`readonly`,``),t.style.position=`fixed`,t.style.top=`0`,t.style.opacity=`0`,document.body.appendChild(t),t.focus(),t.select();let n=document.execCommand(`copy`);return t.remove(),n}catch{return!1}}function Lt(e){It(e).then(e=>Q(e?`Copied`:`copy unavailable in this browser context`))}function Rt(){return{connection:{status:`disconnected`,host:``,port:0,ssl:!1,username:``,msfVersion:``,workspace:``},hosts:[],services:[],sessions:{},jobs:{},routes:[],creds:[],loot:[],moduleRanks:{},operators:[],events:[]}}function zt(e){return{...Rt(),...e,hosts:e?.hosts??[],services:e?.services??[],sessions:e?.sessions??{},jobs:e?.jobs??{},routes:e?.routes??[],creds:e?.creds??[],loot:e?.loot??[],moduleRanks:e?.moduleRanks??{},operators:e?.operators??[],events:e?.events??[]}}function Bt(e,t){switch(t.resource){case`connection`:e.connection=t.connection;break;case`hosts`:e.hosts=t.hosts??[];break;case`services`:e.services=t.services??[];break;case`sessions`:e.sessions=t.sessions??{};break;case`jobs`:e.jobs=t.jobs??{};break;case`routes`:e.routes=t.routes??[];break;case`creds`:e.creds=t.creds??[];break;case`loot`:e.loot=t.loot??[];break;case`modules`:e.modules=t.modules;break;case`moduleRanks`:e.moduleRanks={...e.moduleRanks??{},...t.moduleRanks??{}};break;case`operators`:e.operators=t.operators??[];break;case`console`:e.console=t.console;break;case`interact`:e.interact=t.interact;break;default:console.warn(`unknown resource`,t.resource)}}function Vt(e){let t=new Map;for(let n of Object.values(e.sessions)){if(!n)continue;let e=n.targetHost||n.sessionHost;if(!e)continue;let r=t.get(e)??[];r.push(n),t.set(e,r)}return t}function Ht(e){let t=new Map;for(let n of e.creds){if(!n||!n.host)continue;let e=t.get(n.host)??[];e.push(n),t.set(n.host,e)}return t}var[$,Ut]=x(Rt()),[Wt,Gt]=x(`connecting`),[Kt,qt]=x(!1),[Jt,Yt]=x(``),[Xt,Zt]=x(!1);b(()=>{Y.on(`hello`,e=>{qt(!!e.team),Yt(typeof e.version==`string`?e.version:``),e.team&&Ve()&&Y.command(`operator.join`).catch(()=>{})}),Y.on(`snapshot`,e=>Ut(zt(e.state))),Y.on(`resource`,e=>Ut(t=>{let n={...t};return Bt(n,e),n})),Y.on(`open`,()=>Gt(`open`)),Y.on(`closed`,()=>Gt(`closed`)),Y.on(`proto_mismatch`,()=>Zt(!0))});var Qt=b(()=>w(()=>Vt($()))),$t=b(()=>w(()=>Ht($()))),en=U(`<p>`),tn=U(`<div class=empty-state><div class=empty-symbol><i aria-hidden=true></i></div><h2>`);function nn(e){return(()=>{var t=tn(),n=t.firstChild,r=n.firstChild,i=n.nextSibling;return K(i,()=>e.title),K(t,z(V,{get when(){return e.body},get children(){var t=en();return K(t,()=>e.body),t}}),null),K(t,z(V,{get when(){return e.action},get children(){return e.action}}),null),S(()=>we(r,`ph ph-${e.icon}`)),t})()}var rn=U(`<nav class=tree>`),an=U(`<nav class=tree><div class=filterbox><i aria-hidden=true class="ph ph-magnifying-glass"></i><input placeholder="Filter modules"autocomplete=off aria-label="Filter modules"></div><ul></ul><div class=noresult>No module matches that filter.`),on=U(`<span class=cnt>`),sn=U(`<li><button class="trow branch"><i aria-hidden=true></i><i aria-hidden=true></i><span class=tlabel>/</span></button><ul>`),cn=U(`<li><button class="trow leaf"><i aria-hidden=true class="ph ph-file-code mfil"></i><span class=tlabel>`),ln=U(`<span>`);function un(e){let[t,n]=x(``),[r,i]=x(new Set([`exploit`])),a=w(()=>{let e=$().modules;return e?At(e):null}),o=w(()=>a()?Nt(a(),t()):new Set),s=w(()=>{let e=$().modules;return e?[e.exploits,e.auxiliary,e.post,e.payloads,e.encoders,e.nops,e.evasion].reduce((e,t)=>e+(t?.length??0),0):0});function c(e){i(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})}function l(t,n,r){let i=[{head:n.name,sub:n.path}];n.children.length===0&&n.path!==r&&i.push({icon:`rocket-launch`,label:`Launch…`,fn:()=>e.onLaunch(r,n.path)}),i.push({sep:!0},{icon:`copy`,label:`Copy path`,fn:()=>Lt(n.path)}),gt(t,i)}return z(V,{get when(){return a()},get fallback(){return(()=>{var e=rn();return K(e,z(nn,{icon:`tree`,title:`No modules`,get body(){return s()===0?`Connect to Metasploit to browse available modules.`:`No modules match this build's catalogue.`}})),e})()},children:i=>(()=>{var a=an(),s=a.firstChild,u=s.firstChild.nextSibling,d=s.nextSibling;return u.$$input=e=>n(e.currentTarget.value),G(u,`spellcheck`,!1),K(d,z(B,{get each(){return i().children},children:n=>z(dn,{node:n,get type(){return n.type},open:r,toggle:c,get keep(){return o()},get query(){return t()},onMenu:l,get onLaunch(){return e.onLaunch}})})),S(()=>a.classList.toggle(`filtered`,!!(t()&&o().size===0))),S(()=>u.value=t()),a})()})}function dn(e){let t=()=>e.query?e.keep.has(e.node.path):e.open().has(e.node.path),n=()=>!e.query||e.keep.has(e.node.path),r=()=>Mt(e.node.children,t()).filter(t=>!e.query||e.keep.has(t.path));return z(V,{get when(){return n()},get children(){var n=sn(),i=n.firstChild,a=i.firstChild,o=a.nextSibling,s=o.nextSibling,c=s.firstChild,l=i.nextSibling;return i.$$contextmenu=t=>e.onMenu(t,e.node,e.type),i.$$click=()=>e.toggle(e.node.path),K(s,()=>e.node.name,c),K(i,z(V,{get when(){return e.node.count>0},get children(){var t=on();return K(t,()=>e.node.count.toLocaleString()),t}}),null),K(l,z(B,{get each(){return r()},children:t=>t.children.length===0?(()=>{var n=cn(),r=n.firstChild,i=r.firstChild.nextSibling;return r.$$contextmenu=n=>e.onMenu(n,t,e.type),r.$$click=()=>e.onLaunch(e.type,t.path),K(i,()=>t.name),K(r,z(V,{get when(){return Pt($().moduleRanks?.[`${e.type}/${t.path}`])},children:e=>(()=>{var t=ln();return K(t,()=>e().label),S(()=>we(t,`rankchip${e().hot?` hot`:``}`)),t})()}),null),S(()=>G(r,`title`,t.path)),n})():z(dn,{node:t,get type(){return e.type},get open(){return e.open},get toggle(){return e.toggle},get keep(){return e.keep},get query(){return e.query},get onMenu(){return e.onMenu},get onLaunch(){return e.onLaunch}})})),S(e=>{var n=!!t(),r=t(),s=`ph ${t()?`ph-caret-down`:`ph-caret-right`} caret`,c=`ph ph-folder${t()?`-open`:``} fic`,u=!!t();return n!==e.e&&i.classList.toggle(`expanded`,e.e=n),r!==e.t&&G(i,`aria-expanded`,e.t=r),s!==e.a&&we(a,e.a=s),c!==e.o&&we(o,e.o=c),u!==e.i&&l.classList.toggle(`open`,e.i=u),e},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0}),n}})}W([`input`,`click`,`contextmenu`]);function fn(e,t){let n=t.trim();if(!n||e.at(-1)===n)return e;let r=[...e,n];return r.length>500?r.slice(r.length-500):r}function pn(e,t,n){let r=n===`up`?Math.min(t+1,e.length-1):Math.max(t-1,-1);return{idx:r,text:r===-1?``:e[e.length-1-r]??``}}function mn(e,t){if(t.length===0)return null;if(t.length===1)return t[0]===e?null:t[0];let n=t[0];for(let e of t.slice(1))for(;!e.startsWith(n);)n=n.slice(0,-1);return n.length>e.length?n:null}var hn=U(`<input autocomplete=off>`),gn=U(`<div class=console><div class=inputline><span class=pr>`),_n=U(`<div class="cl out">`),vn=U(`<span>framework is busy`);function yn(e){let[t,n]=x(``),[r,i]=x([]),[a,o]=x(-1),s,c,l=()=>e.output().split(` +`);C(()=>{e.output(),s&&(s.scrollTop=s.scrollHeight)}),C(D(()=>e.target?.()??``,()=>{n(``),i([]),o(-1)},{defer:!0}));function u(){if(e.busy)return;let r=t();if(!r.trim()){n(``);return}e.write(r,e.target?.()),i(e=>fn(e,r)),o(-1),n(``)}async function d(i){if(i.key===`Enter`){i.preventDefault(),u();return}if(i.key===`Tab`){i.preventDefault();let r=t();if(!(r.split(/\s+/).at(-1)??``))return;try{let i=await e.tabComplete(r);if(t()!==r)return;let a=mn(r,i);a!==null&&n(a)}catch{}return}if(i.key===`ArrowUp`||i.key===`ArrowDown`){i.preventDefault();let e=r();if(!e.length)return;let t=pn(e,a(),i.key===`ArrowUp`?`up`:`down`);o(t.idx),n(t.text)}}return(()=>{var r=gn(),i=r.firstChild,a=i.firstChild;r.$$click=()=>c?.focus();var o=s;return typeof o==`function`?Oe(o,r):s=r,K(r,z(B,{get each(){return l()},children:e=>(()=>{var t=_n();return K(t,e||`\xA0`),t})()}),i),K(a,()=>e.prompt),K(i,z(V,{get when(){return!e.busy},get fallback(){return vn()},get children(){var r=hn();r.$$keydown=e=>void d(e),r.$$input=e=>n(e.currentTarget.value);var i=c;return typeof i==`function`?Oe(i,r):c=r,G(r,`spellcheck`,!1),S(()=>G(r,`placeholder`,e.placeholder??`type a framework command`)),S(()=>r.value=t()),r}}),null),r})()}W([`click`,`input`,`keydown`]);var bn=`modulepreload`,xn=function(e){return`/`+e},Sn={},Cn=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=xn(t,n),t=s(t),t in Sn)return;Sn[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:bn,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},wn=`multi/manage/autoroute`;function Tn(e){if(!Number.isInteger(e)||e<0||e>32)return;let t=e=>e>=8?255:e<=0?0:256-2**(8-e);return[0,8,16,24].map(n=>t(e-n)).join(`.`)}function En(e){let t=e.lastIndexOf(`/`);if(t<0)return{subnet:e};let n=Tn(Number(e.slice(t+1)));return n===void 0?{subnet:e}:{subnet:e.slice(0,t),netmask:n}}function Dn(e){let t=e.trim();if(t===``)return;let n=Number(t);return Tn(n)===void 0?void 0:n}function On(e){let t=e.trim().split(`.`);return t.length===4&&t.every(e=>/^\d{1,3}$/.test(e)&&Number(e)<=255)}function kn(e){return{SESSION:e,CMD:`autoadd`}}function An(e,t,n){return{SESSION:e,CMD:`add`,SUBNET:t,NETMASK:Tn(n)??`255.255.255.0`}}function jn(e,t){let n={SESSION:e,CMD:`delete`,SUBNET:t.subnet};return t.netmask&&(n.NETMASK=t.netmask),n}function Mn(e,t){let n=e.filter(e=>e.subnet&&e.sessionId);if(n.length===0)return[];let r=[{head:n[0].subnet,sub:`pivot route`}];for(let e of n)r.push({icon:`signpost`,label:`Remove route via session ${e.sessionId}`,danger:!0,fn:()=>t(e)});return r}async function Nn(e){let{ws:t}=await Cn(async()=>{let{ws:e}=await Promise.resolve().then(()=>Ue);return{ws:e}},void 0);await t.command(`module.execute`,{type:`post`,name:wn,options:e})}function Pn(e){return!e.state||e.state===`open`}function Fn(e){let t=(e.osName||``).toLowerCase();return t.includes(`windows`)?{key:`win`,label:(e.osName||`windows`).replace(`Microsoft Windows`,`WIN`).replace(`Windows Server`,`SRV`).replace(`Windows`,`WIN`).toUpperCase()}:[`linux`,`ubuntu`,`debian`,`centos`,`red hat`,`redhat`,`fedora`,`suse`].some(e=>t.includes(e))?{key:`linux`,label:(e.osName||`linux`).toUpperCase().slice(0,14)}:{key:`other`,label:(e.osName||`unknown`).toUpperCase().slice(0,14)}}var In=26,Ln=36,Rn=40;function zn(e,t){return e.length<=t?e:e.slice(0,Math.max(1,t-1))+`…`}var Bn=/^\d{1,3}$/;function Vn(e){let t=e.split(`.`);return Hn(e)===void 0?`other`:`${t[0]}.${t[1]}.${t[2]}`}function Hn(e){let t=e.split(`.`);if(t.length!==4)return;let n=0;for(let e of t){if(!Bn.test(e))return;let t=Number(e);if(t>255)return;n=n*256+t}return n}function Un(e,t){let n=e.lastIndexOf(`/`),r=n<0?`32`:e.slice(n+1);if(r===``)return!1;let i=Number(r);if(!Number.isInteger(i)||i<0||i>32)return!1;let a=Hn(n<0?e:e.slice(0,n)),o=Hn(t);if(a===void 0||o===void 0)return!1;let s=i===0?0:4294967295<<32-i>>>0;return(a&s)>>>0==(o&s)>>>0}function Wn(e,t,n){return`${e.filter(e=>!!e).map(e=>e.address).join(`|`)}#${t.filter(e=>!!e).map(e=>{let t=n[e.sessionId],r=t&&(t.targetHost||t.sessionHost)||``;return`${e.subnet}>${e.sessionId}@${r}`}).join(`|`)}`}function Gn(e){let t=new Map;for(let n of e){let e=Vn(n.address),r=t.get(e)??[];r.push(n.address),t.set(e,r)}return new Map([...t].sort(([e],[t])=>e===`other`?1:t===`other`?-1:e.localeCompare(t,void 0,{numeric:!0})).map(([e,t])=>[e,t.sort((e,t)=>Number(e.split(`.`)[3]??0)-Number(t.split(`.`)[3]??0))]))}function Kn(e,t=3,n=1){let r=new Map,i=[...Gn(e).values()],a=i.map(e=>Zn(e.length,t)),o=i.map(e=>Qn(e.length,t)),s=$n(a,n),c=Array(n).fill(0);i.forEach((e,t)=>{c[s[t]]=Math.max(c[s[t]],o[t])});let l=[],u=0;for(let e=0;e<n;e++)l.push(u),u+=c[e]+Rn;let d=Array(n).fill(0);return i.forEach((e,n)=>{let i=s[n],o=d[i];e.forEach((e,n)=>{let a=n%t,s=Math.floor(n/t);r.set(e,{x:l[i]+22+a*266,y:o+66+s*160})}),d[i]=o+a[n]+Rn}),r}function qn(e,t,n=3,r=1){let i=Kn(e,n,r);for(let[e,n]of t)i.has(e)&&i.set(e,n);return i}function Jn(e,t,n,r){let i=new Map(t),a=Kn(e,n,r),o=(e,t)=>{for(let n of i.values())if(e<n.x+240&&e+240>n.x&&t<n.y+124&&t+124>n.y)return!0;return!1},s=new Map;for(let[e,n]of t){let t=Vn(e),r=s.get(t);(!r||n.y<r.y||n.y===r.y&&n.x<r.x)&&s.set(t,{x:n.x,y:n.y})}for(let[e,t]of a){if(i.has(e))continue;let n=s.get(Vn(e));if(!n){i.set(e,t);continue}let r=!1;for(let t=0;t<512&&!r;t++)for(let a=0;a<32&&!r;a++){let s=n.x+a*266,c=n.y+t*160;o(s,c)||(i.set(e,{x:s,y:c}),r=!0)}r||i.set(e,t)}return i}function Yn(e,t=Kn(e)){let n=new Map;for(let[r,i]of Gn(e)){let e=i.map(e=>t.get(e)).filter(e=>!!e);if(e.length===0)continue;let a=Math.min(...e.map(e=>e.x)),o=Math.min(...e.map(e=>e.y)),s=Math.max(...e.map(e=>e.x+240)),c=Math.max(...e.map(e=>e.y+124));n.set(r,{x:a-22,y:o-66,w:s-a+44,h:c-o+66+22,count:i.length})}return n}var Xn=8;function Zn(e,t){let n=Math.ceil(e/t);return 66+n*124+Math.max(0,n-1)*Ln+22}function Qn(e,t){let n=Math.min(e,t);return n*240+(n-1)*In+44}function $n(e,t){let n=Array(t).fill(0);return e.map(e=>{let r=0;for(let e=1;e<t;e++)n[e]<n[r]&&(r=e);return n[r]=n[r]+e+Rn,r})}function er(e,t,n){let r=e.map(e=>Zn(e,t)),i=e.map(e=>Qn(e,t)),a=$n(r,n),o=Array(n).fill(0),s=Array(n).fill(0);return e.forEach((e,t)=>{let n=a[t];o[n]=Math.max(o[n],i[t]),s[n]=s[n]===0?r[t]:s[n]+Rn+r[t]}),{w:o.reduce((e,t)=>e+t,0)+(n-1)*Rn,h:Math.max(0,...s)}}function tr(e,t,n,r){let i=[...Gn(e).values()].map(e=>e.length);if(i.length===0||t<=0||n<=0)return{columns:3,groupColumns:1};let a=Math.max(...i),o={columns:1,groupColumns:1},s=0;for(let e=1;e<=Math.min(Xn,a);e++)for(let a=1;a<=i.length;a++){let c=er(i,e,a),l=Math.min(t/(c.w+r),n/c.h);l>s&&(o={columns:e,groupColumns:a},s=l)}return o}function nr(e,t,n){let r=e.y+18,i=t.y<r?-1:1,a=t.x<n?-1:1,o=Math.min(8,Math.abs(t.y-r)/2);return`M ${e.x} ${e.y} V ${r-8} Q ${e.x} ${r} ${e.x+8} ${r} H ${n-o} Q ${n} ${r} ${n} ${r+i*o} V ${t.y-i*o} Q ${n} ${t.y} ${n+a*o} ${t.y} H ${t.x}`}var rr=`hayduk.topology.positions.v3`;function ir(e=localStorage){try{let t=e.getItem(rr);if(!t)return new Map;let n=JSON.parse(t);if(!n||typeof n!=`object`||Array.isArray(n))return new Map;let r=new Map;for(let[e,t]of Object.entries(n)){let n=t;typeof n?.x==`number`&&Number.isFinite(n.x)&&typeof n.y==`number`&&Number.isFinite(n.y)&&r.set(e,{x:n.x,y:n.y})}return r}catch{return new Map}}function ar(e,t=localStorage){try{t.setItem(rr,JSON.stringify(Object.fromEntries(e)))}catch{}}function or(e,t){let n=new Map;for(let[r,i]of e)t.has(r)&&n.set(r,i);return n}var sr=U(`<span class=map-context-hint role=status>dense map at this size — zoom into a subnet, or use the Services view`),cr=U(`<div class=map-context><span><span class=map-context-dot></span> subnet<span class=map-context-divider>/</span> hosts</span><button title="Arrange hosts to fit the canvas; replaces saved positions"><i aria-hidden=true class="ph ph-graph"></i>Arrange hosts`),lr=U(`<svg><g><g class=topo-zones></g><g class=topo-routes></g><g class=topo-hosts></g><g class=topo-ghosts></svg>`,!1,!0,!1),ur=U(`<svg><g class=topo-chips></svg>`,!1,!0,!1),dr=U(`<svg id=topo role=group aria-label="Campaign network topology"><defs><filter id=topo-node-shadow x=-30% y=-35% width=160% height=180%><feDropShadow dx=0 dy=6 stdDeviation=7 flood-color=#000000 flood-opacity=.32></feDropShadow></filter><marker id=topo-route-arrow markerWidth=8 markerHeight=8 refX=7 refY=4 orient=auto markerUnits=strokeWidth><path d="M 0 0 L 8 4 L 0 8 z">`),fr=U(`<output class=map-scale aria-label="Graph zoom">%`),pr=U(`<svg><text class=topo-zone-route-note>ROUTED NETWORK</svg>`,!1,!0,!1),mr=U(`<svg><circle class=topo-route-port r=4></svg>`,!1,!0,!1),hr=U(`<svg><g class=topo-zone><rect class=topo-zone-panel rx=18></rect><path class=topo-zone-rule></path><g><rect class=topo-zone-icon-bg width=28 height=24 rx=6></rect><circle class=topo-zone-icon cx=8 cy=12 r=2></circle><circle class=topo-zone-icon cx=19 cy=7 r=2></circle><circle class=topo-zone-icon cx=19 cy=17 r=2></circle><path class=topo-zone-link d="M 10 11 L 17 8 M 10 13 L 17 16"></path></g><text class=topo-zone-name></text><text class=topo-zone-meta text-anchor=end> </svg>`,!1,!0,!1),gr=U(`<svg><g><path class=topo-route-halo></path><path class=topo-route-line marker-end=url(#topo-route-arrow)></path><g class=topo-route-label><rect height=20 rx=5></rect><text x=0 y=13>SESSION </svg>`,!1,!0,!1),_r=U(`<svg><g class=topo-node tabIndex=0 role=button><title> service`,!1,!0,!1),vr=U(`ROUTED NETWORKAWAITING DISCOVERY`,!1,!0,!1),yr=U(` · `,!1,!0,!1),br=220,xr=100,Sr=18,Cr=.3;function wr(e){return Math.max(64,40+e.length*7)}function Tr(e,t,n){return Math.min(Math.max(e,t),n)}function Er(e){let t,n,r=0,[i,a]=x({x:0,y:0,s:1}),[o,s]=x(ir()),[c,l]=x({width:900,height:500}),[u,d]=x(0),[f,p]=x(!1),m=w(()=>$().hosts.filter(e=>!!e)),h=w(()=>new Map(m().map(e=>[e.address,e]))),g={columns:3,groupColumns:1},_=w(()=>{let e=$().routes.length>0?320:0;return m(),c(),f()||(g=tr(m(),Math.max(16,c().width-48),Math.max(16,c().height-48),e)),g}),v=w(e=>{let t=_().columns,n=_().groupColumns;if(f()&&e){let r=new Map(e);for(let[e,t]of o())r.set(e,t);return Jn(m(),r,t,n)}return qn(m(),o(),t,n)}),y=w(()=>{let e=new Map;for(let t of $().services)t&&Pn(t)&&e.set(t.host,(e.get(t.host)??0)+1);return e}),b=w(()=>Yn(m(),v())),ee=w(()=>{let e=i(),t=[],n=new Map;if(e.s>=Cr)return n;let r=c().height-20,a=[...b()].map(([t,n])=>({key:t,x:e.x+(n.x+n.w/2)*e.s,y:e.y+(n.y+n.h/2)*e.s})).sort((e,t)=>e.y-t.y);for(let e of a)for(let i=0;i<6;i++){let a=e.x+i*170,o=i===0?e.y:16;for(;t.some(e=>a-78e.x1&&o-16e.y1);)o+=34;if(o+16<=r||i===5){o=Math.min(o,Math.max(16,r-16)),t.push({x1:a-78,y1:o-16,x2:a+78,y2:o+16}),n.set(e.key,{x:a,y:o});break}}return n}),te=w(()=>{let e=Qt(),t=$t();return n=>({sessions:e.get(n)??[],login:(t.get(n)?.length??0)>0})}),T=w(()=>{let e=$(),t=[];for(let n of e.routes){if(!n?.subnet||!n.sessionId)continue;let e=new Set;for(let t of m())Un(n.subnet,t.address)&&e.add(Vn(t.address));t.push({route:{subnet:n.subnet,sessionId:n.sessionId},covered:e})}return t}),E=w(()=>{let e=new Set;for(let{covered:t}of T())for(let n of t)e.add(n);return e}),ne=w(()=>{let e=new Map,t=b(),n=v(),r=$(),i=Math.max(0,...[...t.values()].map(e=>e.x+e.w))+100;for(let{route:t,covered:a}of T()){if(a.size>0||e.has(t.subnet))continue;let o=r.sessions[t.sessionId],s=o?.targetHost||o?.sessionHost,c=s?n.get(s):void 0,l=c?c.y+62-xr/2:e.size*118,u=[...e.values()].at(-1);u&&(l=Math.max(l,u.y+xr+Sr)),e.set(t.subnet,{label:t.subnet,x:i,y:l})}return e}),re=w(()=>{let e=$(),t=v(),n=b(),r=ne(),i=[];for(let{route:a,covered:o}of T()){let s=e.sessions[a.sessionId],c=s?.targetHost||s?.sessionHost,l=c?t.get(c):void 0;if(!l)continue;let u={x:l.x+120,y:l.y+124},d=Math.max(...[...n.values()].map(e=>e.x+e.w))+48+i.length*14,f=!1;for(let e of o){let t=n.get(e);if(!t)continue;let r={x:t.x+t.w,y:t.y+30};i.push({sessionID:a.sessionId,subnet:a.subnet,from:u,to:r,lane:d,label:{x:d,y:r.y-30},labelW:wr(a.sessionId)}),f=!0}if(f)continue;let p=r.get(a.subnet);if(p){let e={x:p.x,y:p.y+xr/2};i.push({sessionID:a.sessionId,subnet:a.subnet,from:u,to:e,lane:d,label:{x:p.x+br/2,y:p.y-30},labelW:wr(a.sessionId)})}}return i}),A=w(()=>{let e=[...[...b().values()].map(e=>({x:e.x,y:e.y,w:e.w,h:e.h})),...[...ne().values()].map(e=>({x:e.x,y:e.y,w:br,h:xr}))];if(e.length===0)return;let t=Math.min(...e.map(e=>e.x)),n=Math.min(...e.map(e=>e.y)),r=Math.max(...e.map(e=>e.x+e.w)),i=Math.max(...e.map(e=>e.y+e.h));for(let e of re())r=Math.max(r,e.lane+10,e.label.x+e.labelW/2),t=Math.min(t,e.label.x-e.labelW/2),n=Math.min(n,e.label.y),t=Math.min(t,e.from.x,e.to.x),n=Math.min(n,e.from.y,e.to.y),r=Math.max(r,e.from.x,e.to.x),i=Math.max(i,e.from.y+18,e.to.y);return{x:t,y:n,w:r-t,h:i-n}});function ie(){if(!t)return;p(!1);let e=A();if(!e)return;let n=t.getBoundingClientRect();if(n.width===0||n.height===0)return;let r={left:24,right:24,top:24,bottom:24},i=Math.max(16,n.width-r.left-r.right),o=Math.max(16,n.height-r.top-r.bottom),s=Math.min(i/e.w,o/e.h,1.12);a({s,x:r.left+(i-e.w*s)/2-e.x*s,y:r.top+(o-e.h*s)/2-e.y*s}),d(s)}function ae(e){if(!t)return;let n=t.getBoundingClientRect(),r=Math.max(.31,Math.min(n.width/e.w,n.height/e.h,1.12));p(!0),a({s:r,x:n.width/2-(e.x+e.w/2)*r,y:n.height/2-(e.y+e.h/2)*r})}function j(){cancelAnimationFrame(r),r=requestAnimationFrame(ie)}function oe(e,n,r){if(!t)return;let o=t.getBoundingClientRect(),s=n??o.width/2,c=r??o.height/2,l=i(),u=Tr(l.s*e,.005,4);p(!0),a({s:u,x:s-(s-l.x)*(u/l.s),y:c-(c-l.y)*(u/l.s)})}function M(e){e.preventDefault();let n=t.getBoundingClientRect();oe(e.deltaY<0?1.12:1/1.12,e.clientX-n.left,e.clientY-n.top)}function se(e,n){let r=t.getBoundingClientRect(),a=i();return{x:(e-r.left-a.x)/a.s,y:(n-r.top-a.y)/a.s}}function ce(e){Nn(jn(e.sessionId,En(e.subnet))).catch(t=>Q(t?.message??`could not remove route ${e.subnet}`))}function N(e,t){gt(t,Mn([{subnet:e.subnet,sessionId:e.sessionID}],ce))}function P(e,t){gt(t,Mn($().routes.filter(t=>!!t&&t.subnet===e).map(e=>({subnet:e.subnet,sessionId:e.sessionId})),ce))}function le(t,n){e.onSelect(t);let r=te()(t),i=[{head:t,sub:`host`}];for(let t of r.sessions)i.push({icon:`terminal-window`,label:`Interact with session ${t.id}`,fn:()=>e.onInteract(t.id)});i.push({icon:`rocket-launch`,label:`Launch…`,fn:()=>e.onLaunch(t)},{icon:`key`,label:`Login as…`,fn:()=>e.onLogin(t)},{sep:!0},{icon:`copy`,label:`Copy address`,hint:t,fn:()=>Lt(t)}),gt(n,i)}let F,I,L;function ue(e){if(e.button!==0)return;let n=e.target.closest(`.topo-zone-chip`);n?.dataset.zone&&(L={key:n.dataset.zone,startX:e.clientX,startY:e.clientY,moved:!1});let r=e.target.closest(`.topo-node`);if(r?.dataset.address){let t=se(e.clientX,e.clientY),n=v().get(r.dataset.address);if(!n)return;I={address:r.dataset.address,dx:t.x-n.x,dy:t.y-n.y,startX:e.clientX,startY:e.clientY,moved:!1,origin:{x:n.x,y:n.y},snapshot:new Map(o())}}else if(!L){let n=i();F={startX:e.clientX,startY:e.clientY,viewX:n.x,viewY:n.y,moved:!1},t.classList.add(`panning`)}t.setPointerCapture(e.pointerId)}function R(e){if(L){Math.abs(e.clientX-L.startX)+Math.abs(e.clientY-L.startY)>4&&(L.moved=!0);return}if(I){if(Math.abs(e.clientX-I.startX)+Math.abs(e.clientY-I.startY)>4&&(I.moved=!0),!I.moved)return;p(!0);let t=se(e.clientX,e.clientY),n={x:t.x-I.dx,y:t.y-I.dy};s(e=>new Map(e).set(I.address,n));return}F&&(Math.abs(e.clientX-F.startX)+Math.abs(e.clientY-F.startY)>4&&(F.moved=!0),F.moved&&p(!0),a(t=>({...t,x:F.viewX+e.clientX-F.startX,y:F.viewY+e.clientY-F.startY})))}function de(){if(L&&!L.moved){let e=b().get(L.key);e&&ae(e)}if(I&&!I.moved&&e.onSelect(I.address),I?.moved){let e=new Set(m().map(e=>e.address)),t=or(o(),e);s(t),ar(t)}F&&!F.moved&&e.onSelect(void 0),L=void 0,I=void 0,F=void 0,t.classList.remove(`panning`)}function fe(){if(I){let e=new Map(I.snapshot);e.has(I.address)||e.set(I.address,I.origin),s(e)}L=void 0,I=void 0,F=void 0,t.classList.remove(`panning`)}return O(()=>{let e=()=>ie(),i=e=>oe(e.detail?.k??1);window.addEventListener(`hayduk:fit`,e),window.addEventListener(`hayduk:zoom`,i),n=new ResizeObserver(()=>{let e=t.getBoundingClientRect();l({width:e.width,height:e.height}),f()||j()}),n.observe(t),j(),k(()=>{cancelAnimationFrame(r),n?.disconnect(),window.removeEventListener(`hayduk:fit`,e),window.removeEventListener(`hayduk:zoom`,i)})}),C(D(w(()=>{let e=$();return Wn(e.hosts,e.routes,e.sessions)}),()=>{f()||j()},{defer:!0})),[(()=>{var e=cr(),t=e.firstChild,n=t.firstChild.nextSibling,r=n.nextSibling,i=r.nextSibling,a=t.nextSibling;return K(t,()=>b().size,n),K(t,()=>b().size===1?``:`s`,r),K(t,()=>m().length,i),K(e,z(V,{get when(){return H(()=>m().length>0&&u()>0)()&&u()<.1},get children(){return sr()}}),a),a.$$click=()=>{s(new Map),ar(new Map),j()},e})(),(()=>{var n=dr();n.firstChild,n.addEventListener(`pointercancel`,fe),n.$$pointerup=de,n.$$pointermove=R,n.$$pointerdown=ue,n.addEventListener(`wheel`,M);var r=t;return typeof r==`function`?Oe(r,n):t=n,K(n,z(V,{get when(){return A()},get children(){var t=lr(),n=t.firstChild,r=n.nextSibling,a=r.nextSibling,o=a.nextSibling;return K(n,z(B,{get each(){return[...b().keys()]},children:e=>{let t=()=>b().get(e);return(()=>{var n=hr(),r=n.firstChild,i=r.nextSibling,a=i.nextSibling,o=a.nextSibling,s=o.nextSibling,c=s.firstChild;return K(o,e===`other`?`OTHER HOSTS`:`${e}.0/24`),K(s,()=>t().count,c),K(s,()=>t().count===1?`HOST`:`HOSTS`,null),K(n,z(V,{get when(){return E().has(e)},get children(){return[(()=>{var e=pr();return S(n=>{var r=t().x+60,i=t().y+44;return r!==n.e&&G(e,`x`,n.e=r),i!==n.t&&G(e,`y`,n.t=i),n},{e:void 0,t:void 0}),e})(),(()=>{var e=mr();return S(n=>{var r=t().x+t().w,i=t().y+30;return r!==n.e&&G(e,`cx`,n.e=r),i!==n.t&&G(e,`cy`,n.t=i),n},{e:void 0,t:void 0}),e})()]}}),null),S(e=>{var n=t().x,c=t().y,l=t().w,u=t().h,d=`M ${t().x+20} ${t().y+49} H ${t().x+t().w-20}`,f=`translate(${t().x+20} ${t().y+15})`,p=t().x+60,m=t().y+31,h=t().x+t().w-20,g=t().y+31;return n!==e.e&&G(r,`x`,e.e=n),c!==e.t&&G(r,`y`,e.t=c),l!==e.a&&G(r,`width`,e.a=l),u!==e.o&&G(r,`height`,e.o=u),d!==e.i&&G(i,`d`,e.i=d),f!==e.n&&G(a,`transform`,e.n=f),p!==e.s&&G(o,`x`,e.s=p),m!==e.h&&G(o,`y`,e.h=m),h!==e.r&&G(s,`x`,e.r=h),g!==e.d&&G(s,`y`,e.d=g),e},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0,s:void 0,h:void 0,r:void 0,d:void 0}),n})()}})),K(r,z(B,{get each(){return re()},children:e=>(()=>{var t=gr(),n=t.firstChild,r=n.nextSibling,i=r.nextSibling,a=i.firstChild,o=a.nextSibling;return o.firstChild,t.$$contextmenu=t=>N(e,t),K(o,()=>e.sessionID,null),S(t=>{var o=nr(e.from,e.to,e.lane),s=nr(e.from,e.to,e.lane),c=`translate(${e.label.x} ${e.label.y})`,l=-e.labelW/2,u=e.labelW;return o!==t.e&&G(n,`d`,t.e=o),s!==t.t&&G(r,`d`,t.t=s),c!==t.a&&G(i,`transform`,t.a=c),l!==t.o&&G(a,`x`,t.o=l),u!==t.i&&G(a,`width`,t.i=u),t},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0}),t})()})),K(a,z(B,{get each(){return m().map(e=>e.address)},children:t=>{let n=()=>h().get(t),r=()=>v().get(t),i=()=>te()(t),a=()=>Fn(n());return z(V,{get when(){return r()},children:r=>(()=>{var o=_r(),s=o.firstChild,c=s.nextSibling,l=c.nextSibling,u=l.nextSibling,d=u.nextSibling,f=d.nextSibling,p=f.nextSibling,m=p.nextSibling.nextSibling,h=m.nextSibling,g=h.firstChild;return o.$$keydown=n=>{(n.key===`Enter`||n.key===` `)&&(n.preventDefault(),e.onSelect(t))},o.$$contextmenu=e=>le(t,e),G(o,`data-address`,t),K(s,()=>`${n().name||t} · ${t}`),G(c,`width`,240),G(c,`height`,124),G(l,`d`,`M 0 86 H 240`),K(d,()=>zn(a().label,14)),K(f,()=>zn(n().name||t,19)),K(p,()=>zn(t,25)),K(m,(()=>{var e=H(()=>i().sessions.length>0);return()=>e()?`${i().sessions.length} live session${i().sessions.length===1?``:`s`}`:i().login?`Login available`:`Discovered`})()),G(h,`x`,226),K(h,()=>y().get(t)??0,g),K(h,()=>y().get(t)===1?``:`s`,null),S(s=>{var c=e.selected()===t,l=i().sessions.length>0,f=!!(i().login&&i().sessions.length===0),p=`translate(${r().x} ${r().y})`,m=`${n().name||t}, ${t}`,h=`topo-device ${a().key}`,g=`topo-node-os ${a().key}`;return c!==s.e&&o.classList.toggle(`selected`,s.e=c),l!==s.t&&o.classList.toggle(`access`,s.t=l),f!==s.a&&o.classList.toggle(`login`,s.a=f),p!==s.o&&G(o,`transform`,s.o=p),m!==s.i&&G(o,`aria-label`,s.i=m),h!==s.n&&G(u,`class`,s.n=h),g!==s.s&&G(d,`class`,s.s=g),s},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0,s:void 0}),o})()})}})),K(o,z(B,{get each(){return Array.from(ne().keys())},children:e=>(()=>{var t=vr(),n=t.firstChild.nextSibling.nextSibling.nextSibling;return t.$$contextmenu=t=>P(e,t),K(n,()=>zn(ne().get(e).label,19)),S(()=>G(t,`transform`,`translate(${ne().get(e).x} ${ne().get(e).y})`)),t})()})),S(()=>G(t,`transform`,`translate(${i().x} ${i().y}) scale(${i().s})`)),t}}),null),K(n,z(V,{get when(){return ee().size>0},get children(){var e=ur();return K(e,z(B,{get each(){return[...b().keys()]},children:e=>{let t=()=>ee().get(e),n=()=>b().get(e)?.count??0;return z(V,{get when(){return t()},children:t=>(()=>{var r=yr(),i=r.firstChild.nextSibling,a=i.firstChild;return r.$$keydown=t=>{if(t.key===`Enter`||t.key===` `){t.preventDefault();let n=b().get(e);n&&ae(n)}},G(r,`data-zone`,e),G(r,`aria-label`,`Zoom to ${e===`other`?`other hosts`:`subnet ${e}.0/24`}`),K(i,e===`other`?`OTHER HOSTS`:`${e}.0/24`,a),K(i,n,null),S(()=>G(r,`transform`,`translate(${t().x} ${t().y})`)),r})()})}})),e}}),null),S(()=>n.classList.toggle(`lod`,i().s{var e=fr(),t=e.firstChild;return K(e,()=>Math.round(i().s*100),t),e})()]}W([`click`,`pointerdown`,`pointermove`,`pointerup`,`contextmenu`,`keydown`]);var Dr=U(`
`),Or=U(``),jr=U(`
`),kr=U(`
`),Ar=U(`
`);function Mr(e){return(()=>{var t=Dr(),n=t.firstChild.firstChild,r=n.firstChild,i=n.nextSibling;return K(r,z(B,{get each(){return e.columns},children:e=>(()=>{var t=Or();return K(t,()=>e.label),S(n=>Ee(t,e.width?`width:${e.width}`:``,n)),t})()})),K(i,z(V,{get when(){return e.rows.length>0},get fallback(){return(()=>{var t=kr(),n=t.firstChild;return K(n,z(nn,{get icon(){return e.emptyIcon??`table`},get title(){return e.emptyTitle},get body(){return e.empty}})),S(()=>G(n,`colspan`,e.columns.length)),t})()},get children(){return z(B,{get each(){return e.rows},children:t=>(()=>{var n=Ar();return n.$$contextmenu=n=>e.onRowContextMenu?.(t,n),n.$$click=()=>e.onRowClick?.(t),K(n,z(B,{get each(){return e.columns},children:e=>(()=>{var n=jr();return K(n,(()=>{var n=H(()=>!!e.render);return()=>n()?e.render(t):String(t[e.key]??``)})()),S(()=>we(n,e.mono?`m`:``)),n})()})),S(()=>n.classList.toggle(`sel`,e.selectedKey?.()===e.rowKey(t))),n})()})}})),t})()}W([`click`,`contextmenu`]);function Nr(e=3e4){let[t,n]=x(Date.now());return O(()=>{let t=window.setInterval(()=>n(Date.now()),e);k(()=>window.clearInterval(t))}),t}var Pr=U(`

Route traffic to another network through this session. The new pivot appears on the topology as a dashed route edge.`),Fr=U(`